Files
gozerosandCursor 0c45b86d67 chore: 初始化 eightbf-analysis-go(对齐 Java 会商作业服务)
Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-03 00:13:46 +08:00

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)
})
}