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
+167 -1
View File
@@ -6,6 +6,7 @@ import (
"errors"
"io"
"net/http"
"net/url"
"strconv"
"strings"
@@ -36,6 +37,8 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
"/v1/resources",
"/openapi.yaml",
"/admin/schedules",
"/admin/translation/tasks",
"/admin/translation/handoff",
},
Controls: []string{
"/admin/control/reload",
@@ -50,6 +53,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
"/admin/control/schedule-remove",
"/admin/control/schedule-run",
"/admin/control/translation-task-update",
"/admin/control/translation-worker-run",
"/admin/control/translation-proofread",
},
}
@@ -82,6 +86,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
s.handleAdminTranslationTaskUpdate(w, r)
return
}
if action == "translation-worker-run" {
s.handleAdminTranslationWorkerRun(w, r)
return
}
if action == "translation-proofread" {
s.handleAdminTranslationProofread(w, r)
return
@@ -187,6 +195,34 @@ func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http
})
}
func (s *Server) handleAdminTranslationWorkerRun(w http.ResponseWriter, r *http.Request) {
backend, ok := s.backend.(TranslationBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
return
}
var params backendrpc.TranslationWorkerRunParams
if !decodeAdminTranslationJSON(w, r, &params) {
return
}
if err := validateTranslationWorkerRunParams(params); err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_params", err.Error())
return
}
result, err := backend.TranslationWorkerRun(r.Context(), params)
if err != nil {
s.writeControlBackendError(w, "translation-worker-run", err)
return
}
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
Service: "bat-api",
Action: "translation-worker-run",
RPCMethod: "translation.worker.run",
Status: "accepted",
Result: result,
})
}
func (s *Server) handleAdminTranslationProofread(w http.ResponseWriter, r *http.Request) {
backend, ok := s.backend.(TranslationBackend)
if !ok || backend == nil {
@@ -207,6 +243,63 @@ func (s *Server) handleAdminTranslationProofread(w http.ResponseWriter, r *http.
})
}
func (s *Server) handleAdminTranslationTasks(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.(TranslationBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
return
}
params, err := translationTaskListParams(r)
if err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_query", err.Error())
return
}
result, err := backend.TranslationTasks(r.Context(), params)
if err != nil {
s.writeControlBackendError(w, "translation-tasks", 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) handleAdminTranslationHandoff(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.(TranslationBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
return
}
result, err := backend.TranslationHandoff(r.Context())
if err != nil {
s.writeControlBackendError(w, "translation-handoff", 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")
@@ -254,6 +347,63 @@ func scheduleListParams(r *http.Request) (backendrpc.ScheduleListParams, error)
return params, nil
}
func translationTaskListParams(r *http.Request) (backendrpc.TranslationTaskListParams, error) {
query := r.URL.Query()
params := backendrpc.TranslationTaskListParams{
TaskID: strings.TrimSpace(query.Get("task_id")),
ReleaseID: firstTrimmedQuery(query, "release_id", "official_release_id"),
Destination: strings.TrimSpace(query.Get("destination")),
PathPattern: strings.TrimSpace(query.Get("path_pattern")),
ArchiveEntry: strings.TrimSpace(query.Get("archive_entry")),
Status: firstTrimmedQuery(query, "status", "task_status"),
WorkerStatus: strings.TrimSpace(query.Get("worker_status")),
ParseStatus: strings.TrimSpace(query.Get("parse_status")),
Format: firstTrimmedQuery(query, "format", "text_unit_format"),
}
if raw := strings.TrimSpace(query.Get("offset")); raw != "" {
offset, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return backendrpc.TranslationTaskListParams{}, errors.New("offset must be a non-negative integer")
}
params.Offset = &offset
}
if raw := strings.TrimSpace(query.Get("limit")); raw != "" {
limit, err := strconv.ParseUint(raw, 10, 64)
if err != nil || limit == 0 || limit > 1000 {
return backendrpc.TranslationTaskListParams{}, errors.New("limit must be in 1..=1000")
}
params.Limit = &limit
}
if raw := strings.TrimSpace(query.Get("has_reason")); raw != "" {
hasReason, err := strconv.ParseBool(raw)
if err != nil {
return backendrpc.TranslationTaskListParams{}, errors.New("has_reason must be a boolean")
}
params.HasReason = &hasReason
}
if raw := strings.TrimSpace(query.Get("has_failure_reason")); raw != "" {
hasFailureReason, err := strconv.ParseBool(raw)
if err != nil {
return backendrpc.TranslationTaskListParams{}, errors.New("has_failure_reason must be a boolean")
}
params.HasFailureReason = &hasFailureReason
}
return params, nil
}
func firstTrimmedQuery(query url.Values, keys ...string) string {
for _, key := range keys {
values := query[key]
if len(values) == 0 {
continue
}
if value := strings.TrimSpace(values[0]); value != "" {
return value
}
}
return ""
}
func (s *Server) handleAdminScheduleControl(w http.ResponseWriter, r *http.Request, action string) {
backend, ok := s.backend.(ScheduleBackend)
if !ok || backend == nil {
@@ -325,7 +475,23 @@ func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any)
}
func decodeAdminTranslationJSON(w http.ResponseWriter, r *http.Request, target any) bool {
return decodeAdminJSON(w, r, target, "invalid_translation_params", "translation task request")
return decodeAdminJSON(w, r, target, "invalid_translation_params", "translation request")
}
func validateTranslationWorkerRunParams(params backendrpc.TranslationWorkerRunParams) error {
if params.Concurrency != nil && (*params.Concurrency < 1 || *params.Concurrency > 256) {
return errors.New("translation worker concurrency must be in 1..=256")
}
if params.MaxAttempts != nil && *params.MaxAttempts == 0 {
return errors.New("translation worker max_attempts must be greater than 0")
}
if params.LeaseSeconds != nil && *params.LeaseSeconds == 0 {
return errors.New("translation worker lease_seconds must be greater than 0")
}
if params.MaxTasks != nil && *params.MaxTasks == 0 {
return errors.New("translation worker max_tasks must be greater than 0")
}
return nil
}
func decodeAdminJSON(w http.ResponseWriter, r *http.Request, target any, errorCode string, subject string) bool {