chore: 初始化 eightbf-analysis-go(对齐 Java 会商作业服务)
Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
0c45b86d67
29 files changed
+3789
No files matched your search
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ApiResponse 与 Java 一致:{ code, message, data };成功 code=0。
|
||||
type ApiResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func OK(w http.ResponseWriter, data interface{}) {
|
||||
writeJSON(w, http.StatusOK, ApiResponse{Code: 0, Message: "ok", Data: data})
|
||||
}
|
||||
|
||||
func Created(w http.ResponseWriter, data interface{}) {
|
||||
writeJSON(w, http.StatusCreated, ApiResponse{Code: 0, Message: "ok", Data: data})
|
||||
}
|
||||
|
||||
func Fail(w http.ResponseWriter, httpStatus, code int, msg string) {
|
||||
writeJSON(w, httpStatus, ApiResponse{Code: code, Message: msg, Data: nil})
|
||||
}
|
||||
Reference in new issue
Block a user