You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
// handler.go 暴露编排器模块的 HTTP 端点:/api/v1/orchestrator/runs(POST 启动、
|
||||
// GET 列表)、/runs/{id}(GET)、/runs/{id}/pause|resume|cancel(POST)。
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/project"
|
||||
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
// AdminLookup 解析某用户是否为 admin(与 acp/chat/workspace 一致)。
|
||||
type AdminLookup interface {
|
||||
IsAdmin(ctx context.Context, userID uuid.UUID) (bool, error)
|
||||
}
|
||||
|
||||
// Handler 暴露编排器 HTTP 端点。
|
||||
type Handler struct {
|
||||
svc Service
|
||||
resolver middleware.SessionResolver
|
||||
adminLookup AdminLookup
|
||||
}
|
||||
|
||||
// NewHandler 构造 Handler。
|
||||
func NewHandler(svc Service, resolver middleware.SessionResolver, al AdminLookup) *Handler {
|
||||
return &Handler{svc: svc, resolver: resolver, adminLookup: al}
|
||||
}
|
||||
|
||||
// Mount 注册 /api/v1/orchestrator/* 路由。
|
||||
func (h *Handler) Mount(r chi.Router) {
|
||||
r.Route("/api/v1/orchestrator/runs", func(r chi.Router) {
|
||||
r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{}))
|
||||
r.Post("/", h.startRun)
|
||||
r.Get("/", h.listRuns)
|
||||
r.Get("/{id}", h.getRun)
|
||||
r.Post("/{id}/pause", h.pauseRun)
|
||||
r.Post("/{id}/resume", h.resumeRun)
|
||||
r.Post("/{id}/cancel", h.cancelRun)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) caller(r *http.Request) (Caller, error) {
|
||||
uid, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
return Caller{}, errs.New(errs.CodeUnauthorized, "not authenticated")
|
||||
}
|
||||
admin, err := h.adminLookup.IsAdmin(r.Context(), uid)
|
||||
if err != nil {
|
||||
return Caller{}, err
|
||||
}
|
||||
return Caller{UserID: uid, IsAdmin: admin}, nil
|
||||
}
|
||||
|
||||
// ===== DTO =====
|
||||
|
||||
type startRunReq struct {
|
||||
ProjectSlug string `json:"project_slug"`
|
||||
RequirementNumber int `json:"requirement_number"`
|
||||
StartPhase string `json:"start_phase,omitempty"`
|
||||
PhaseAgentKinds map[string]string `json:"phase_agent_kinds,omitempty"`
|
||||
PromptOverrides map[string]string `json:"prompt_overrides,omitempty"`
|
||||
MaxAttemptsPerPhase int `json:"max_attempts_per_phase,omitempty"`
|
||||
MaxDepth int `json:"max_depth,omitempty"`
|
||||
FanOutLimit int `json:"fan_out_limit,omitempty"`
|
||||
}
|
||||
|
||||
type runDTO struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
RequirementID string `json:"requirement_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
Status string `json:"status"`
|
||||
CurrentPhase string `json:"current_phase"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||
}
|
||||
|
||||
func runToDTO(r *Run) runDTO {
|
||||
d := runDTO{
|
||||
ID: r.ID.String(),
|
||||
ProjectID: r.ProjectID.String(),
|
||||
RequirementID: r.RequirementID.String(),
|
||||
WorkspaceID: r.WorkspaceID.String(),
|
||||
OwnerID: r.OwnerID.String(),
|
||||
Status: string(r.Status),
|
||||
CurrentPhase: string(r.CurrentPhase),
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
EndedAt: r.EndedAt,
|
||||
}
|
||||
if r.LastError != nil {
|
||||
d.LastError = *r.LastError
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ===== handlers =====
|
||||
|
||||
func (h *Handler) startRun(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
var body startRunReq
|
||||
if derr := json.NewDecoder(r.Body).Decode(&body); derr != nil {
|
||||
h.writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad json"))
|
||||
return
|
||||
}
|
||||
if body.ProjectSlug == "" || body.RequirementNumber <= 0 {
|
||||
h.writeErr(w, r, errs.New(errs.CodeInvalidInput, "project_slug and requirement_number are required"))
|
||||
return
|
||||
}
|
||||
|
||||
cfg := RunConfig{
|
||||
MaxAttemptsPerPhase: body.MaxAttemptsPerPhase,
|
||||
MaxDepth: body.MaxDepth,
|
||||
FanOutLimit: body.FanOutLimit,
|
||||
}
|
||||
if len(body.PhaseAgentKinds) > 0 {
|
||||
cfg.PhaseAgentKinds = map[project.Phase]uuid.UUID{}
|
||||
for ph, idStr := range body.PhaseAgentKinds {
|
||||
id, perr := uuid.Parse(idStr)
|
||||
if perr != nil {
|
||||
h.writeErr(w, r, errs.Wrap(perr, errs.CodeInvalidInput, "phase_agent_kinds id parse"))
|
||||
return
|
||||
}
|
||||
cfg.PhaseAgentKinds[project.Phase(ph)] = id
|
||||
}
|
||||
}
|
||||
if len(body.PromptOverrides) > 0 {
|
||||
cfg.PromptOverrides = map[project.Phase]string{}
|
||||
for ph, p := range body.PromptOverrides {
|
||||
cfg.PromptOverrides[project.Phase(ph)] = p
|
||||
}
|
||||
}
|
||||
|
||||
run, err := h.svc.StartRun(r.Context(), c, StartRunInput{
|
||||
ProjectSlug: body.ProjectSlug,
|
||||
RequirementNumber: body.RequirementNumber,
|
||||
StartPhase: project.Phase(body.StartPhase),
|
||||
Config: cfg,
|
||||
})
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, runToDTO(run))
|
||||
}
|
||||
|
||||
func (h *Handler) getRun(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if perr != nil {
|
||||
h.writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||
return
|
||||
}
|
||||
run, err := h.svc.GetRun(r.Context(), c, id)
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, runToDTO(run))
|
||||
}
|
||||
|
||||
func (h *Handler) listRuns(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
f := RunFilter{Status: RunStatus(r.URL.Query().Get("status"))}
|
||||
if rid := r.URL.Query().Get("requirement_id"); rid != "" {
|
||||
if id, perr := uuid.Parse(rid); perr == nil {
|
||||
f.RequirementID = &id
|
||||
}
|
||||
}
|
||||
if pid := r.URL.Query().Get("project_id"); pid != "" {
|
||||
if id, perr := uuid.Parse(pid); perr == nil {
|
||||
f.ProjectID = &id
|
||||
}
|
||||
}
|
||||
runs, err := h.svc.ListRuns(r.Context(), c, f)
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
out := make([]runDTO, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
out = append(out, runToDTO(run))
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *Handler) pauseRun(w http.ResponseWriter, r *http.Request) { h.runAction(w, r, h.svc.Pause) }
|
||||
func (h *Handler) resumeRun(w http.ResponseWriter, r *http.Request) { h.runAction(w, r, h.svc.Resume) }
|
||||
func (h *Handler) cancelRun(w http.ResponseWriter, r *http.Request) { h.runAction(w, r, h.svc.Cancel) }
|
||||
|
||||
func (h *Handler) runAction(w http.ResponseWriter, r *http.Request, fn func(context.Context, Caller, uuid.UUID) error) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
h.writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if perr != nil {
|
||||
h.writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||
return
|
||||
}
|
||||
if aerr := fn(r.Context(), c, id); aerr != nil {
|
||||
h.writeErr(w, r, aerr)
|
||||
return
|
||||
}
|
||||
run, gerr := h.svc.GetRun(r.Context(), c, id)
|
||||
if gerr != nil {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, runToDTO(run))
|
||||
}
|
||||
|
||||
func (h *Handler) writeErr(w http.ResponseWriter, r *http.Request, err error) {
|
||||
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
|
||||
}
|
||||
Reference in New Issue
Block a user