Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
4.9 KiB
Go
190 lines
4.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.gozeros.com/wsk/eightbf-analysis-go/internal/service"
|
|
)
|
|
|
|
// Handler HTTP 入口,路径与 Java AnalysisJobController 对齐。
|
|
type Handler struct {
|
|
Jobs *service.JobService
|
|
}
|
|
|
|
func (h *Handler) Routes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /api/v1/analysis/health", h.health)
|
|
mux.HandleFunc("POST /api/v1/analysis/jobs", h.create)
|
|
mux.HandleFunc("GET /api/v1/analysis/jobs/{id}", h.get)
|
|
mux.HandleFunc("GET /api/v1/analysis/jobs", h.getByTask)
|
|
mux.HandleFunc("POST /api/v1/analysis/jobs/{id}/cancel", h.cancel)
|
|
mux.HandleFunc("POST /api/v1/analysis/jobs/{id}/resume", h.resume)
|
|
mux.HandleFunc("POST /api/v1/analysis/jobs/{id}/plan", h.patchPlan)
|
|
mux.HandleFunc("POST /api/v1/analysis/jobs/{id}/inject", h.inject)
|
|
mux.HandleFunc("GET /api/v1/analysis/jobs/{id}/result", h.result)
|
|
}
|
|
|
|
func (h *Handler) caller(r *http.Request) service.Caller {
|
|
return service.NewCaller(r.Header.Get("X-Account-Id"), r.Header.Get("X-Analysis-Role"))
|
|
}
|
|
|
|
func (h *Handler) health(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"service": "eightbf-analysis-go",
|
|
})
|
|
}
|
|
|
|
func (h *Handler) create(w http.ResponseWriter, r *http.Request) {
|
|
var body service.CreateJobRequest
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
job, err := h.Jobs.Create(&body, h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
Created(w, job)
|
|
}
|
|
|
|
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
job, err := h.Jobs.Get(id, h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found")
|
|
return
|
|
}
|
|
OK(w, job)
|
|
}
|
|
|
|
func (h *Handler) getByTask(w http.ResponseWriter, r *http.Request) {
|
|
taskID := strings.TrimSpace(r.URL.Query().Get("taskId"))
|
|
if taskID == "" {
|
|
Fail(w, http.StatusBadRequest, 400, "taskId required")
|
|
return
|
|
}
|
|
latest := r.URL.Query().Get("latest") == "true"
|
|
job, err := h.Jobs.GetByTaskID(taskID, latest, h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found for taskId")
|
|
return
|
|
}
|
|
OK(w, job)
|
|
}
|
|
|
|
func (h *Handler) cancel(w http.ResponseWriter, r *http.Request) {
|
|
job, err := h.Jobs.Cancel(r.PathValue("id"), h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found")
|
|
return
|
|
}
|
|
OK(w, job)
|
|
}
|
|
|
|
func (h *Handler) resume(w http.ResponseWriter, r *http.Request) {
|
|
var body service.ResumeJobRequest
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
job, err := h.Jobs.Resume(r.PathValue("id"), &body, h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found")
|
|
return
|
|
}
|
|
OK(w, job)
|
|
}
|
|
|
|
func (h *Handler) patchPlan(w http.ResponseWriter, r *http.Request) {
|
|
var body service.PatchJobPlanRequest
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
job, err := h.Jobs.PatchPlan(r.PathValue("id"), &body, h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found")
|
|
return
|
|
}
|
|
OK(w, job)
|
|
}
|
|
|
|
func (h *Handler) inject(w http.ResponseWriter, r *http.Request) {
|
|
var body service.InjectJobRequest
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
job, err := h.Jobs.Inject(r.PathValue("id"), &body, h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found")
|
|
return
|
|
}
|
|
OK(w, job)
|
|
}
|
|
|
|
func (h *Handler) result(w http.ResponseWriter, r *http.Request) {
|
|
job, err := h.Jobs.Get(r.PathValue("id"), h.caller(r))
|
|
if err != nil {
|
|
h.mapErr(w, err)
|
|
return
|
|
}
|
|
if job == nil {
|
|
Fail(w, http.StatusNotFound, 404, "job not found")
|
|
return
|
|
}
|
|
OK(w, map[string]interface{}{
|
|
"id": job.ID,
|
|
"status": job.Status,
|
|
"result": job.Result,
|
|
"error": job.Error,
|
|
"dialogues": job.Dialogues,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) mapErr(w http.ResponseWriter, err error) {
|
|
var be *service.BizError
|
|
if errors.As(err, &be) {
|
|
switch {
|
|
case errors.Is(be.Kind, service.ErrForbidden):
|
|
Fail(w, http.StatusForbidden, 403, be.Msg)
|
|
case errors.Is(be.Kind, service.ErrBadState), errors.Is(be.Kind, service.ErrBadArg):
|
|
Fail(w, http.StatusBadRequest, 400, be.Msg)
|
|
default:
|
|
Fail(w, http.StatusInternalServerError, 500, be.Msg)
|
|
}
|
|
return
|
|
}
|
|
Fail(w, http.StatusInternalServerError, 500, err.Error())
|
|
}
|
|
|
|
// WithCORS 本机联调放开跨域(经反代同源时无影响)。
|
|
func WithCORS(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|