mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
+43
-2
@@ -49,6 +49,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/admin/control/schedule-update",
|
||||
"/admin/control/schedule-remove",
|
||||
"/admin/control/schedule-run",
|
||||
"/admin/control/translation-task-update",
|
||||
},
|
||||
}
|
||||
if r.Method == http.MethodHead {
|
||||
@@ -76,6 +77,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminScheduleControl(w, r, action)
|
||||
return
|
||||
}
|
||||
if action == "translation-task-update" {
|
||||
s.handleAdminTranslationTaskUpdate(w, r)
|
||||
return
|
||||
}
|
||||
request, ok := decodeAdminControlRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -149,6 +154,34 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
backend, ok := s.backend.(TranslationBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
||||
return
|
||||
}
|
||||
var params backendrpc.TranslationTaskUpdateParams
|
||||
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(params.TaskID) == "" || strings.TrimSpace(params.Status) == "" {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_params", "translation task update requires task_id and status")
|
||||
return
|
||||
}
|
||||
result, err := backend.TranslationTaskUpdate(r.Context(), params)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "translation-task-update", err)
|
||||
return
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||
Service: "bat-api",
|
||||
Action: "translation-task-update",
|
||||
RPCMethod: "translation.task.update",
|
||||
Status: "accepted",
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||
@@ -263,6 +296,14 @@ func (s *Server) requireAdminToken(w http.ResponseWriter, r *http.Request) bool
|
||||
}
|
||||
|
||||
func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
||||
return decodeAdminJSON(w, r, target, "invalid_schedule_params", "schedule request")
|
||||
}
|
||||
|
||||
func decodeAdminTranslationJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
||||
return decodeAdminJSON(w, r, target, "invalid_translation_params", "translation task request")
|
||||
}
|
||||
|
||||
func decodeAdminJSON(w http.ResponseWriter, r *http.Request, target any, errorCode string, subject string) bool {
|
||||
if r.Body == nil {
|
||||
return true
|
||||
}
|
||||
@@ -272,11 +313,11 @@ func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return true
|
||||
}
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_schedule_params", "schedule request must be a JSON object")
|
||||
writeErrorJSON(w, http.StatusBadRequest, errorCode, subject+" must be a JSON object")
|
||||
return false
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_schedule_params", "schedule request must contain exactly one JSON object")
|
||||
writeErrorJSON(w, http.StatusBadRequest, errorCode, subject+" must contain exactly one JSON object")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -588,6 +588,11 @@ func (b *controlBackend) CatalogRefresh(ctx context.Context, force bool) (*backe
|
||||
return &backendrpc.TaskAccepted{TaskID: "task-catalog-refresh-1", Kind: "catalog.refresh"}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) {
|
||||
b.calls = append(b.calls, "translation.task.update")
|
||||
return json.RawMessage(`{"task_status":"` + params.Status + `"}`), nil
|
||||
}
|
||||
|
||||
type scheduleBackend struct {
|
||||
*controlBackend
|
||||
scheduleCalls []string
|
||||
@@ -1150,6 +1155,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
||||
{name: "force sync", action: "sync", body: `{"force":true}`, rpcMethod: "resource.sync", call: "resource.sync"},
|
||||
{name: "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"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1172,6 +1178,14 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/admin/control/translation-task-update", strings.NewReader(`{"task_id":""}`))
|
||||
request.Header.Set("Authorization", "Bearer control-token")
|
||||
recorder := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("invalid translation update status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminControlRejectsUnauthenticatedDangerousAndUnsupportedActions(t *testing.T) {
|
||||
|
||||
@@ -148,7 +148,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run]
|
||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, translation-task-update]
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@@ -191,6 +191,14 @@ paths:
|
||||
type: boolean
|
||||
enabled:
|
||||
type: boolean
|
||||
task_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
failure_reason:
|
||||
type: string
|
||||
provider_run_id:
|
||||
type: string
|
||||
responses:
|
||||
"202":
|
||||
description: Rust bat accepted the control request.
|
||||
|
||||
@@ -49,6 +49,12 @@ type ScheduleBackend interface {
|
||||
ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error)
|
||||
}
|
||||
|
||||
// TranslationBackend exposes the narrow provider-worker state mutation used
|
||||
// by the dashboard. It does not create arbitrary translation jobs.
|
||||
type TranslationBackend interface {
|
||||
TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error)
|
||||
}
|
||||
|
||||
// RPCClient adapts *backendrpc.Client to Backend.
|
||||
type RPCClient struct {
|
||||
Client *backendrpc.Client
|
||||
@@ -105,6 +111,9 @@ func (r RPCClient) ScheduleRemove(ctx context.Context, params backendrpc.Schedul
|
||||
func (r RPCClient) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error) {
|
||||
return r.Client.ScheduleRun(ctx, params)
|
||||
}
|
||||
func (r RPCClient) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) {
|
||||
return r.Client.TranslationTaskUpdate(ctx, params)
|
||||
}
|
||||
func (r RPCClient) ParseStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return r.Client.ParseStatus(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user