mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:04:55 +08:00
+357
-2
@@ -15,11 +15,17 @@ import (
|
||||
|
||||
const adminControlMaxBodyBytes = 1024
|
||||
const adminScheduleMaxBodyBytes = 64 * 1024
|
||||
const adminDefaultLogTail = 200
|
||||
const adminMaxLogTail = 2000
|
||||
|
||||
type adminControlRequest struct {
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
type adminTaskRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminIndex(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")
|
||||
@@ -36,7 +42,16 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/v1/release",
|
||||
"/v1/resources",
|
||||
"/openapi.yaml",
|
||||
"/admin/dashboard/",
|
||||
"/admin/schedules",
|
||||
"/admin/tasks",
|
||||
"/admin/tasks/status",
|
||||
"/admin/tasks/logs",
|
||||
"/admin/diagnostics",
|
||||
"/admin/logs",
|
||||
"/admin/parse/status",
|
||||
"/admin/parse/text-units",
|
||||
"/admin/parse/errors",
|
||||
"/admin/translation/tasks",
|
||||
"/admin/translation/handoff",
|
||||
"/admin/translation/status",
|
||||
@@ -53,6 +68,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/task-cancel",
|
||||
"/admin/control/translation-task-update",
|
||||
"/admin/control/translation-worker-run",
|
||||
"/admin/control/translation-proofread",
|
||||
@@ -85,6 +101,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminScheduleControl(w, r, action)
|
||||
return
|
||||
}
|
||||
if action == "task-cancel" {
|
||||
s.handleAdminTaskCancel(w, r)
|
||||
return
|
||||
}
|
||||
if action == "translation-task-update" {
|
||||
s.handleAdminTranslationTaskUpdate(w, r)
|
||||
return
|
||||
@@ -188,8 +208,8 @@ func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http
|
||||
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")
|
||||
if err := validateTranslationTaskUpdateParams(params); err != nil {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_params", err.Error())
|
||||
return
|
||||
}
|
||||
result, err := backend.TranslationTaskUpdate(r.Context(), params)
|
||||
@@ -332,6 +352,253 @@ func (s *Server) handleAdminLocalizedStatus(w http.ResponseWriter, r *http.Reque
|
||||
writeNoStoreJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminDiagnostics(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
|
||||
}
|
||||
if s.backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "diagnostics_backend_unavailable", "Rust bat diagnostics backend is unavailable")
|
||||
return
|
||||
}
|
||||
result, err := s.backend.DaemonDoctor(r.Context())
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "diagnostics", 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) handleAdminLogs(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.(DaemonLogsBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "diagnostics_backend_unavailable", "Rust bat log backend is unavailable")
|
||||
return
|
||||
}
|
||||
tail, err := adminLogTail(r)
|
||||
if err != nil {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_log_query", err.Error())
|
||||
return
|
||||
}
|
||||
result, err := backend.DaemonLogs(r.Context(), tail)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "daemon-logs", 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) handleAdminTasks(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.(TaskBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
||||
return
|
||||
}
|
||||
result, err := backend.TaskList(r.Context())
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "task-list", 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) handleAdminTaskStatus(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.(TaskBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
||||
return
|
||||
}
|
||||
taskID, err := adminTaskIDQuery(r)
|
||||
if err != nil {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_task_query", err.Error())
|
||||
return
|
||||
}
|
||||
result, err := backend.TaskStatus(r.Context(), taskID)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "task-status", 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) handleAdminTaskLogs(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.(TaskBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
||||
return
|
||||
}
|
||||
taskID, err := adminTaskIDQuery(r)
|
||||
if err != nil {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_task_query", err.Error())
|
||||
return
|
||||
}
|
||||
result, err := backend.TaskLogs(r.Context(), taskID)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "task-logs", 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) handleAdminParseStatus(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.(ParseBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "parse_backend_unavailable", "Rust bat parse backend is unavailable")
|
||||
return
|
||||
}
|
||||
result, err := backend.ParseStatus(r.Context())
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "parse-status", 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) handleAdminParseTextUnits(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminParseTextUnitQuery(w, r, false)
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminParseErrors(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminParseTextUnitQuery(w, r, true)
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminParseTextUnitQuery(w http.ResponseWriter, r *http.Request, errorsOnly bool) {
|
||||
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.(ParseBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "parse_backend_unavailable", "Rust bat parse backend is unavailable")
|
||||
return
|
||||
}
|
||||
query, err := parseTextUnitQueryParams(r)
|
||||
if err != nil {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_parse_query", err.Error())
|
||||
return
|
||||
}
|
||||
var result json.RawMessage
|
||||
if errorsOnly {
|
||||
result, err = backend.ParseErrors(r.Context(), query)
|
||||
} else {
|
||||
result, err = backend.ParseTextUnits(r.Context(), query)
|
||||
}
|
||||
if err != nil {
|
||||
action := "parse-text-units"
|
||||
if errorsOnly {
|
||||
action = "parse-errors"
|
||||
}
|
||||
s.writeControlBackendError(w, action, 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) handleAdminTaskCancel(w http.ResponseWriter, r *http.Request) {
|
||||
backend, ok := s.backend.(TaskBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
||||
return
|
||||
}
|
||||
var params adminTaskRequest
|
||||
if !decodeAdminTaskJSON(w, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
taskID := strings.TrimSpace(params.TaskID)
|
||||
if taskID == "" {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_task_params", "task cancel requires task_id")
|
||||
return
|
||||
}
|
||||
result, err := backend.TaskCancel(r.Context(), taskID)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "task-cancel", err)
|
||||
return
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||
Service: "bat-api",
|
||||
Action: "task-cancel",
|
||||
RPCMethod: "task.cancel",
|
||||
Status: "accepted",
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -480,6 +747,46 @@ func translationTaskListParams(r *http.Request) (backendrpc.TranslationTaskListP
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
|
||||
query := r.URL.Query()
|
||||
params := backendrpc.TextUnitQueryParams{
|
||||
Destination: strings.TrimSpace(query.Get("destination")),
|
||||
PathPattern: strings.TrimSpace(query.Get("path_pattern")),
|
||||
ArchiveEntry: strings.TrimSpace(query.Get("archive_entry")),
|
||||
FieldPath: strings.TrimSpace(query.Get("field_path")),
|
||||
Format: strings.TrimSpace(query.Get("format")),
|
||||
}
|
||||
if raw := strings.TrimSpace(query.Get("offset")); raw != "" {
|
||||
offset, err := strconv.ParseInt(raw, 10, 32)
|
||||
if err != nil || offset < 0 {
|
||||
return backendrpc.TextUnitQueryParams{}, errors.New("offset must be a non-negative integer")
|
||||
}
|
||||
params.Offset = int(offset)
|
||||
}
|
||||
if raw := strings.TrimSpace(query.Get("limit")); raw != "" {
|
||||
limit, err := strconv.ParseInt(raw, 10, 32)
|
||||
if err != nil || limit < 1 || limit > 1000 {
|
||||
return backendrpc.TextUnitQueryParams{}, errors.New("limit must be in 1..=1000")
|
||||
}
|
||||
params.Limit = int(limit)
|
||||
}
|
||||
if raw := strings.TrimSpace(query.Get("path_id")); raw != "" {
|
||||
pathID, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
return backendrpc.TextUnitQueryParams{}, errors.New("path_id must be a signed integer")
|
||||
}
|
||||
params.PathID = &pathID
|
||||
}
|
||||
if raw := strings.TrimSpace(query.Get("class_id")); raw != "" {
|
||||
classID, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return backendrpc.TextUnitQueryParams{}, errors.New("class_id must be a signed integer")
|
||||
}
|
||||
params.ClassID = &classID
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func firstTrimmedQuery(query url.Values, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
values := query[key]
|
||||
@@ -493,6 +800,26 @@ func firstTrimmedQuery(query url.Values, keys ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func adminLogTail(r *http.Request) (int, error) {
|
||||
raw := strings.TrimSpace(r.URL.Query().Get("tail"))
|
||||
if raw == "" {
|
||||
return adminDefaultLogTail, nil
|
||||
}
|
||||
tail, err := strconv.Atoi(raw)
|
||||
if err != nil || tail < 1 || tail > adminMaxLogTail {
|
||||
return 0, errors.New("tail must be in 1..=2000")
|
||||
}
|
||||
return tail, nil
|
||||
}
|
||||
|
||||
func adminTaskIDQuery(r *http.Request) (string, error) {
|
||||
taskID := strings.TrimSpace(r.URL.Query().Get("task_id"))
|
||||
if taskID == "" {
|
||||
return "", errors.New("task_id is required")
|
||||
}
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminScheduleControl(w http.ResponseWriter, r *http.Request, action string) {
|
||||
backend, ok := s.backend.(ScheduleBackend)
|
||||
if !ok || backend == nil {
|
||||
@@ -567,6 +894,34 @@ func decodeAdminTranslationJSON(w http.ResponseWriter, r *http.Request, target a
|
||||
return decodeAdminJSON(w, r, target, "invalid_translation_params", "translation request")
|
||||
}
|
||||
|
||||
func decodeAdminTaskJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
||||
return decodeAdminJSON(w, r, target, "invalid_task_params", "task request")
|
||||
}
|
||||
|
||||
func validateTranslationTaskUpdateParams(params backendrpc.TranslationTaskUpdateParams) error {
|
||||
if strings.TrimSpace(params.TaskID) == "" || strings.TrimSpace(params.Status) == "" {
|
||||
return errors.New("translation task update requires task_id and status")
|
||||
}
|
||||
if len(params.TranslationResults) == 0 {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(params.Status) != "completed" {
|
||||
return errors.New("translation_results can only be submitted with completed status")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(params.TranslationResults))
|
||||
for _, result := range params.TranslationResults {
|
||||
unitID := strings.TrimSpace(result.UnitID)
|
||||
if unitID == "" {
|
||||
return errors.New("translation_results unit_id is required")
|
||||
}
|
||||
if _, ok := seen[unitID]; ok {
|
||||
return errors.New("translation_results unit_id must be unique")
|
||||
}
|
||||
seen[unitID] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user