diff --git a/apps/api/internal/httpapi/handlers.go b/apps/api/internal/httpapi/handlers.go index 373ef97..6d6adda 100644 --- a/apps/api/internal/httpapi/handlers.go +++ b/apps/api/internal/httpapi/handlers.go @@ -1598,6 +1598,10 @@ func boolValue(body map[string]any, key string) bool { func (s *Server) getTask(w http.ResponseWriter, r *http.Request) { task, err := s.store.GetTask(r.Context(), r.PathValue("taskID")) if err == nil { + cancelState := runner.DescribeTaskCancellation(task) + task.Cancellable = &cancelState.Cancellable + task.Submitted = &cancelState.Submitted + task.Message = cancelState.Message writeJSON(w, http.StatusOK, task) return } diff --git a/apps/api/internal/runner/task_cancel.go b/apps/api/internal/runner/task_cancel.go index e35400d..600f778 100644 --- a/apps/api/internal/runner/task_cancel.go +++ b/apps/api/internal/runner/task_cancel.go @@ -20,6 +20,25 @@ type TaskCancelResult struct { Message string `json:"message"` } +func DescribeTaskCancellation(task store.GatewayTask) TaskCancelResult { + if taskCancelTerminalStatus(task.Status) { + return taskCancelUnavailable(task, "任务已结束,无法取消") + } + if strings.TrimSpace(task.RemoteTaskID) != "" { + return taskCancelUnavailable(task, "任务已提交上游,当前不可取消,请继续查询结果") + } + if strings.TrimSpace(task.Status) != "queued" { + return taskCancelUnavailable(task, "任务已开始执行,当前阶段不可取消,请继续查询结果") + } + return TaskCancelResult{ + TaskID: task.ID, + Cancelled: false, + Cancellable: true, + Submitted: false, + Message: "任务仍在本地队列中,可取消", + } +} + func (s *Service) CancelTask(ctx context.Context, taskID string, user *auth.User) (TaskCancelResult, error) { task, err := s.store.GetTask(ctx, taskID) if err != nil { diff --git a/apps/api/internal/runner/task_cancel_test.go b/apps/api/internal/runner/task_cancel_test.go index 15b90aa..680e719 100644 --- a/apps/api/internal/runner/task_cancel_test.go +++ b/apps/api/internal/runner/task_cancel_test.go @@ -76,3 +76,26 @@ func TestTaskCancelUnavailableReportsSubmittedFromRemoteTaskID(t *testing.T) { t.Fatalf("remote task id should report submitted: %+v", result) } } + +func TestDescribeTaskCancellationReportsQueuedTaskAsCancellable(t *testing.T) { + result := DescribeTaskCancellation(store.GatewayTask{ID: "task-queued", Status: "queued"}) + + if result.TaskID != "task-queued" { + t.Fatalf("unexpected task id: %+v", result) + } + if result.Cancelled || !result.Cancellable || result.Submitted { + t.Fatalf("queued local task should be cancellable and not submitted: %+v", result) + } +} + +func TestDescribeTaskCancellationReportsRemoteTaskAsSubmitted(t *testing.T) { + result := DescribeTaskCancellation(store.GatewayTask{ + ID: "task-remote", + Status: "running", + RemoteTaskID: "remote-1", + }) + + if result.Cancelled || result.Cancellable || !result.Submitted { + t.Fatalf("remote task should be submitted and non-cancellable: %+v", result) + } +} diff --git a/apps/api/internal/store/postgres.go b/apps/api/internal/store/postgres.go index 82b11b1..e06342e 100644 --- a/apps/api/internal/store/postgres.go +++ b/apps/api/internal/store/postgres.go @@ -446,6 +446,9 @@ type GatewayTask struct { AsyncMode bool `json:"asyncMode"` RiverJobID int64 `json:"riverJobId,omitempty"` Status string `json:"status"` + Cancellable *bool `json:"cancellable,omitempty"` + Submitted *bool `json:"submitted,omitempty"` + Message string `json:"message,omitempty"` AttemptCount int `json:"attemptCount"` RemoteTaskID string `json:"remoteTaskId,omitempty"` RemoteTaskPayload map[string]any `json:"remoteTaskPayload,omitempty"`