feat(i18n): 接入翻译 provider worker
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

Closes #44
This commit is contained in:
2026-08-30 21:13:31 +08:00
parent 7f465523e1
commit f441f1810e
29 changed files with 3815 additions and 130 deletions
+123 -1
View File
@@ -588,7 +588,8 @@ func (f *fakeBackend) ResourceManifest(ctx context.Context, offset int, limit in
type controlBackend struct {
*fakeBackend
calls []string
calls []string
translationTaskListParams []backendrpc.TranslationTaskListParams
}
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
@@ -631,6 +632,38 @@ func (b *controlBackend) TranslationTaskUpdate(ctx context.Context, params backe
return json.RawMessage(`{"task_status":"` + params.Status + `"}`), nil
}
func (b *controlBackend) TranslationTasks(ctx context.Context, params backendrpc.TranslationTaskListParams) (json.RawMessage, error) {
b.calls = append(b.calls, "translation.tasks")
b.translationTaskListParams = append(b.translationTaskListParams, params)
return json.RawMessage(`{"tasks":[{"task_id":"textunit/v-current/Scenario","worker_status":"failed","failure_reason":"provider rejected payload"}]}`), nil
}
func (b *controlBackend) TranslationHandoff(ctx context.Context) (json.RawMessage, error) {
b.calls = append(b.calls, "translation.handoff")
return json.RawMessage(`{"jobs":[],"provider_runs":[]}`), nil
}
func (b *controlBackend) TranslationWorkerRun(ctx context.Context, params backendrpc.TranslationWorkerRunParams) (*backendrpc.TranslationWorkerRunResult, error) {
b.calls = append(b.calls, "translation.worker.run")
concurrency := uint64(8)
if params.Concurrency != nil {
concurrency = *params.Concurrency
}
return &backendrpc.TranslationWorkerRunResult{
TaskID: "task-translation-worker-1",
Kind: "translation.worker.run",
Worker: backendrpc.TranslationWorkerConfig{
Provider: params.Provider,
FixturePath: params.FixturePath,
Concurrency: concurrency,
MaxAttempts: 3,
LeaseSeconds: 300,
RetryBackoffSeconds: 5,
WorkerID: params.WorkerID,
},
}, nil
}
func (b *controlBackend) TranslationProofread(ctx context.Context) (json.RawMessage, error) {
b.calls = append(b.calls, "translation.proofread")
return json.RawMessage(`{"translation_workflow_status":"manual_proofreading"}`), nil
@@ -743,6 +776,81 @@ func TestAdminScheduleEndpointsProxyAuthenticatedRequests(t *testing.T) {
}
}
func TestAdminTranslationQueryEndpointsProxyAuthenticatedRequests(t *testing.T) {
cfg := DefaultConfig()
cfg.AuthToken = "translation-token"
if err := cfg.Normalize(); err != nil {
t.Fatal(err)
}
backend := &controlBackend{fakeBackend: &fakeBackend{}}
s := NewServer(cfg, backend, nil)
request := httptest.NewRequest(
http.MethodGet,
"/admin/translation/tasks?offset=10&limit=25&task_id=textunit%2Fv-current%2FScenario&release_id=v-current&destination=MediaResources%2FGameData%2FScenario.zip&path_pattern=Scenario&archive_entry=ScenarioExcelTable.json&status=queued_offline&worker_status=failed&parse_status=ok&format=json&has_reason=true&has_failure_reason=false",
nil,
)
request.Header.Set("Authorization", "Bearer translation-token")
recorder := httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("tasks status=%d body=%s", recorder.Code, recorder.Body.String())
}
if !json.Valid(recorder.Body.Bytes()) || !strings.Contains(recorder.Body.String(), "failure_reason") {
t.Fatalf("tasks body=%s", recorder.Body.String())
}
if len(backend.translationTaskListParams) != 1 {
t.Fatalf("translation task query params=%#v", backend.translationTaskListParams)
}
params := backend.translationTaskListParams[0]
if params.Offset == nil || *params.Offset != 10 ||
params.Limit == nil || *params.Limit != 25 ||
params.TaskID != "textunit/v-current/Scenario" ||
params.ReleaseID != "v-current" ||
params.Destination != "MediaResources/GameData/Scenario.zip" ||
params.PathPattern != "Scenario" ||
params.ArchiveEntry != "ScenarioExcelTable.json" ||
params.Status != "queued_offline" ||
params.WorkerStatus != "failed" ||
params.ParseStatus != "ok" ||
params.Format != "json" ||
params.HasReason == nil || !*params.HasReason ||
params.HasFailureReason == nil || *params.HasFailureReason {
t.Fatalf("params=%#v", params)
}
request = httptest.NewRequest(http.MethodGet, "/admin/translation/handoff", nil)
request.Header.Set("Authorization", "Bearer translation-token")
recorder = httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("handoff status=%d body=%s", recorder.Code, recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), "provider_runs") {
t.Fatalf("handoff body=%s", recorder.Body.String())
}
if len(backend.calls) < 2 ||
backend.calls[len(backend.calls)-2] != "translation.tasks" ||
backend.calls[len(backend.calls)-1] != "translation.handoff" {
t.Fatalf("calls=%v", backend.calls)
}
request = httptest.NewRequest(http.MethodGet, "/admin/translation/tasks?limit=0", nil)
request.Header.Set("Authorization", "Bearer translation-token")
recorder = httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("invalid task query status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = httptest.NewRequest(http.MethodGet, "/admin/translation/tasks", nil)
recorder = httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated tasks status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestDiscoverCallsStatusBeforeDoctor(t *testing.T) {
root := fixtureRoot(t)
bytes := uint64(20)
@@ -1281,6 +1389,11 @@ func TestOpenAPIAndAdminReservedEndpoints(t *testing.T) {
if len(admin.Controls) == 0 || admin.Controls[0] != "/admin/control/reload" {
t.Fatalf("admin controls=%v", admin.Controls)
}
links := strings.Join(admin.Links, "\n")
if !strings.Contains(links, "/admin/translation/tasks") ||
!strings.Contains(links, "/admin/translation/handoff") {
t.Fatalf("admin links=%v", admin.Links)
}
}
func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
@@ -1305,6 +1418,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
{name: "repair", action: "repair", rpcMethod: "resource.repair", call: "resource.repair"},
{name: "catalog refresh", action: "catalog-refresh", rpcMethod: "catalog.refresh", call: "catalog.refresh"},
{name: "translation task update", action: "translation-task-update", body: `{"task_id":"textunit/v-current/Scenario","status":"failed","failure_reason":"provider rejected payload","provider_run_id":"provider-run-1"}`, rpcMethod: "translation.task.update", call: "translation.task.update"},
{name: "translation 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"},
}
for _, tc := range tests {
@@ -1336,6 +1450,14 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
if recorder.Code != http.StatusBadRequest {
t.Fatalf("invalid translation update status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = httptest.NewRequest(http.MethodPost, "/admin/control/translation-worker-run", strings.NewReader(`{"concurrency":0}`))
request.Header.Set("Authorization", "Bearer control-token")
recorder = httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("invalid translation worker status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestAdminControlRejectsUnauthenticatedDangerousAndUnsupportedActions(t *testing.T) {