package server import ( "encoding/json" "errors" "io" "net/http" "os" "path" "path/filepath" "strings" "codex-agent-manager/internal/agents" "codex-agent-manager/internal/app" "codex-agent-manager/internal/codexhome" "codex-agent-manager/internal/projects" "codex-agent-manager/internal/runtime" "codex-agent-manager/internal/workflow" ) const agentDraftBodyLimit = 1 << 20 func New(cfg app.Config) http.Handler { mux := http.NewServeMux() agentStore := agents.Store{CodexHome: cfg.CodexHome} projectStore := projects.Store{CodexHome: cfg.CodexHome} runtimeStore := runtime.Store{CodexHome: cfg.CodexHome} workspaceRoot := cfg.WorkspaceRoot if workspaceRoot == "" { workspaceRoot = "." } workflowStore := workflow.Store{WorkspaceRoot: workspaceRoot, Runtime: runtimeStore} mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) return } items, err := agentStore.List() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) }) mux.HandleFunc("/api/agents/", func(w http.ResponseWriter, r *http.Request) { id, action, ok := parseAgentActionPath(r.URL.Path) if !ok { http.NotFound(w, r) return } if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) return } switch action { case "validate": handleAgentValidate(w, r, agentStore, id) case "write": handleAgentWrite(w, r, agentStore, id) default: http.NotFound(w, r) } }) mux.HandleFunc("/api/projects", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) return } items, err := projectStore.List() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) }) mux.HandleFunc("/api/runtime/threads", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) return } snapshot, err := runtimeStore.Snapshot() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "items": snapshot.Threads, "edges": snapshot.SpawnEdges, "goals": snapshot.Goals, "source": snapshot.Source, "sources": snapshot.Sources, }) }) mux.HandleFunc("/api/workflow/events", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) return } view, err := workflowStore.View() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "items": view.Events, "handoffEdges": view.HandoffEdges, "phases": view.Phases, "source": view.Source, }) }) mux.HandleFunc("/api/", http.NotFound) if cfg.StaticDir != "" { mux.HandleFunc("/", staticFrontendHandler(cfg.StaticDir)) } return mux } type validateRequest struct { Content string `json:"content"` } type writeRequest struct { Content string `json:"content"` ExpectedHash string `json:"expectedHash"` } func parseAgentActionPath(path string) (string, string, bool) { rest := strings.TrimPrefix(path, "/api/agents/") parts := strings.Split(rest, "/") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return "", "", false } return parts[0], parts[1], true } func handleAgentValidate(w http.ResponseWriter, r *http.Request, store agents.Store, id string) { var body validateRequest if !decodeAgentJSON(w, r, &body) { return } result, err := store.ValidateDraft(id, body.Content) if err != nil { writeAgentError(w, err) return } writeJSON(w, http.StatusOK, result) } func handleAgentWrite(w http.ResponseWriter, r *http.Request, store agents.Store, id string) { var body writeRequest if !decodeAgentJSON(w, r, &body) { return } result, err := store.WriteDraft(id, body.Content, body.ExpectedHash) if err != nil { writeAgentError(w, err) return } writeJSON(w, http.StatusOK, result) } func decodeAgentJSON(w http.ResponseWriter, r *http.Request, dest any) bool { r.Body = http.MaxBytesReader(w, r.Body, agentDraftBodyLimit) decoder := json.NewDecoder(r.Body) if err := decoder.Decode(dest); err != nil { writeAgentDecodeError(w, err) return false } var extra any if err := decoder.Decode(&extra); err != io.EOF { if err != nil { writeAgentDecodeError(w, err) return false } writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请求体不是有效 JSON"}) return false } return true } func writeAgentDecodeError(w http.ResponseWriter, err error) { var maxBytesErr *http.MaxBytesError if errors.As(err, &maxBytesErr) { writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "请求体过大"}) return } writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请求体不是有效 JSON"}) } func writeAgentError(w http.ResponseWriter, err error) { status := http.StatusBadRequest message := "写回失败" if errors.Is(err, agents.ErrWriteConflict) { status = http.StatusConflict message = "内容已变化,请重新校验" } else if errors.Is(err, codexhome.ErrForbiddenPath) || errors.Is(err, codexhome.ErrOutsideCodexHome) { status = http.StatusForbidden message = "路径不安全" } else if os.IsNotExist(err) { status = http.StatusNotFound message = "目标智能体不存在" } else if strings.Contains(err.Error(), "第 ") || strings.Contains(err.Error(), "TOML") || strings.Contains(err.Error(), "字符串字段") || strings.Contains(err.Error(), "仅支持字符串字段") { message = "TOML 无效:" + err.Error() } writeJSON(w, status, map[string]string{"error": message}) } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func staticFrontendHandler(staticDir string) http.HandlerFunc { fileServer := http.FileServer(http.Dir(staticDir)) indexPath := filepath.Join(staticDir, "index.html") return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"}) return } cleanPath := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/") if cleanPath == "" { http.ServeFile(w, r, indexPath) return } target := filepath.Join(staticDir, cleanPath) if info, err := os.Stat(target); err == nil && !info.IsDir() { fileServer.ServeHTTP(w, r) return } http.ServeFile(w, r, indexPath) } }