Files
foundry-core/internal/platform/canvas_ai_cancel.go
T
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

159 lines
5.1 KiB
Go

package platform
import (
"database/sql"
"encoding/json"
"io"
"net/http"
)
func (s *service) cancelCanvasAIRun(w http.ResponseWriter, r *http.Request, project, account string) error {
if r.Method != http.MethodPost {
reject(w, 405, "method_not_allowed")
return nil
}
key, id := canvasIdempotencyKey(r), r.PathValue("run")
if !uuidSlugPattern.MatchString(key) || !uuidSlugPattern.MatchString(id) {
reject(w, 400, "invalid_ai_request")
return nil
}
raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 4096))
if err != nil {
reject(w, 413, "context_too_large")
return nil
}
input := map[string]any{}
if len(raw) > 0 && (json.Unmarshal(raw, &input) != nil || input == nil || len(input) > 0) {
reject(w, 400, "invalid_ai_request")
return nil
}
hash := payloadDigest(input)
workspace, board := r.PathValue("workspace"), r.PathValue("material")
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
return err
}
defer tx.Rollback()
_, role, err := lockMaterial(r.Context(), tx, project, workspace, board, account)
if err != nil {
return err
}
if role == "viewer" {
reject(w, 403, "forbidden")
return nil
}
var status string
var rawPlan json.RawMessage
err = tx.QueryRowContext(r.Context(), `SELECT status,plan FROM canvas_ai_runs WHERE project=$1 AND workspace_id=$2 AND board_id=$3 AND id=$4 FOR UPDATE`, project, workspace, board, id).Scan(&status, &rawPlan)
if err != nil {
return err
}
var priorHash string
var prior json.RawMessage
err = tx.QueryRowContext(r.Context(), `SELECT payload_hash,response_body FROM canvas_ai_mutations WHERE project=$1 AND workspace_id=$2 AND run_id=$3 AND account_id=$4 AND action='cancel' AND idempotency_key=$5`, project, workspace, id, account, key).Scan(&priorHash, &prior)
if err == nil {
if priorHash != hash {
reject(w, 409, "idempotency_conflict")
return nil
}
reply(w, 200, prior)
return nil
}
if err != sql.ErrNoRows {
return err
}
var plan canvasAIPlan
if err = json.Unmarshal(rawPlan, &plan); err != nil {
return err
}
for _, step := range plan.Steps {
_, role, err := lockMaterial(r.Context(), tx, project, workspace, step.BoardID, account)
if err != nil {
return err
}
if role == "viewer" {
reject(w, 403, "forbidden")
return nil
}
}
if status == "queued" || status == "running" {
_, err = tx.ExecContext(r.Context(), `UPDATE canvas_ai_runs SET status='cancelled',cancel_requested=true,completed_at=now() WHERE project=$1 AND workspace_id=$2 AND id=$3`, project, workspace, id)
if err != nil {
return err
}
rows, err := tx.QueryContext(r.Context(), `UPDATE canvas_ai_steps SET status='cancelled',error_code='generation_cancelled',completed_at=now() WHERE project=$1 AND workspace_id=$2 AND run_id=$3 AND status IN('queued','running') RETURNING id,board_id::text,node_id`, project, workspace, id)
if err != nil {
return err
}
events := []response{}
for rows.Next() {
var step, board, node string
if err = rows.Scan(&step, &board, &node); err != nil {
break
}
events = append(events, response{"step_id": step, "board_id": board, "node_id": node, "status": "cancelled"})
}
if err == nil {
err = rows.Err()
}
rows.Close()
if err != nil {
return err
}
cancelled := map[string]bool{}
for _, event := range events {
cancelled[event["step_id"].(string)] = true
if err = appendAIEvent(r.Context(), tx, project, workspace, id, "step.cancelled", event); err != nil {
return err
}
}
for _, edge := range plan.Edges {
if cancelled[edge.Target] {
if err = appendAIEvent(r.Context(), tx, project, workspace, id, "edge.skipped", response{"logical_edge_id": edge.ID, "status": "skipped"}); err != nil {
return err
}
}
}
status = "cancelled"
if err = appendAIEvent(r.Context(), tx, project, workspace, id, "run.completed", response{"status": status}); err != nil {
return err
}
}
rows, err := tx.QueryContext(r.Context(), `SELECT id,board_id::text,node_id,status,applied_version FROM canvas_ai_steps WHERE project=$1 AND workspace_id=$2 AND run_id=$3 ORDER BY ordinal`, project, workspace, id)
if err != nil {
return err
}
applied, cancelledSteps := []any{}, []any{}
for rows.Next() {
var step, board, node, state string
var version sql.NullInt64
if err = rows.Scan(&step, &board, &node, &state, &version); err != nil {
break
}
if version.Valid {
applied = append(applied, response{"step_id": step, "board_id": board, "node_id": node, "canvas_version": version.Int64})
}
if state == "cancelled" {
cancelledSteps = append(cancelledSteps, step)
}
}
if err == nil {
err = rows.Err()
}
rows.Close()
if err != nil {
return err
}
result := response{"run_id": id, "status": status, "applied": applied, "cancelled_step_ids": cancelledSteps, "worker_stop_grace_ms": 1000}
encoded, _ := json.Marshal(result)
_, err = tx.ExecContext(r.Context(), `INSERT INTO canvas_ai_mutations(project,workspace_id,run_id,account_id,action,idempotency_key,payload_hash,response_body) VALUES($1,$2,$3,$4,'cancel',$5,$6,$7)`, project, workspace, id, account, key, hash, encoded)
if err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
reply(w, 200, result)
return nil
}