// Package hostapi 是 sidecar 与外部进程调用基座的本机 HTTP 面(/host/v1)。 package hostapi import ( "context" "encoding/json" "io" "net/http" "strings" "agent-system-backend/internal/store" core "agent-system-core" ) // Server 依赖由组合根注入。Dispatch/Search/Complete 可为 nil(对应路由返回 503)。 type Server struct { Token string Store *store.Store Dispatch func(ctx context.Context, pluginID, owner, commandID string, params map[string]any) error Search func(ctx context.Context, owner, text string) ([]core.RetrievalHit, error) Complete func(ctx context.Context, owner, prompt string) (string, error) } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("PUT /host/v1/kv/{key}", s.auth(s.putKV)) mux.HandleFunc("GET /host/v1/kv/{key}", s.auth(s.getKV)) mux.HandleFunc("POST /host/v1/audit", s.auth(s.audit)) mux.HandleFunc("POST /host/v1/commands/{id}", s.auth(s.command)) mux.HandleFunc("POST /host/v1/llm", s.auth(s.llm)) mux.HandleFunc("GET /host/v1/knowledge/search", s.auth(s.search)) return mux } func (s *Server) auth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if s.Token != "" { got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if got != s.Token { http.Error(w, "unauthorized", http.StatusUnauthorized) return } } next(w, r) } } func (s *Server) pluginID(r *http.Request) string { id := r.Header.Get("X-Plugin-Id") if id == "" { id = "unknown" } return id } func (s *Server) owner(r *http.Request) string { if o := r.Header.Get("X-Owner"); o != "" { return o } return store.OwnerDefault } func (s *Server) putKV(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err := s.Store.PutPluginKV(s.pluginID(r), s.owner(r), r.PathValue("key"), string(body)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (s *Server) getKV(w http.ResponseWriter, r *http.Request) { v, ok, err := s.Store.GetPluginKV(s.pluginID(r), s.owner(r), r.PathValue("key")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if !ok { http.Error(w, "not found", http.StatusNotFound) return } writeJSON(w, http.StatusOK, map[string]string{"value": v}) } func (s *Server) audit(w http.ResponseWriter, r *http.Request) { var body struct { Action string `json:"action"` Target string `json:"target"` Evidence string `json:"evidence"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Action == "" { http.Error(w, "action required", http.StatusBadRequest) return } if err := s.Store.AppendAuditEvent(store.AuditEvent{ Owner: s.owner(r), Action: body.Action, Actor: s.pluginID(r), Target: body.Target, Evidence: body.Evidence, TraceID: r.Header.Get("X-Trace-Id"), }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (s *Server) command(w http.ResponseWriter, r *http.Request) { if s.Dispatch == nil { http.Error(w, "command bus unavailable", http.StatusServiceUnavailable) return } var body struct { Params map[string]any `json:"params"` } _ = json.NewDecoder(r.Body).Decode(&body) if err := s.Dispatch(r.Context(), s.pluginID(r), s.owner(r), r.PathValue("id"), body.Params); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusNoContent) } func (s *Server) llm(w http.ResponseWriter, r *http.Request) { if s.Complete == nil { http.Error(w, "llm route not configured", http.StatusServiceUnavailable) return } var body struct { Prompt string `json:"prompt"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Prompt) == "" { http.Error(w, "prompt required", http.StatusBadRequest) return } text, err := s.Complete(r.Context(), s.owner(r), body.Prompt) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, map[string]string{"text": text}) } func (s *Server) search(w http.ResponseWriter, r *http.Request) { if s.Search == nil { http.Error(w, "retriever unavailable", http.StatusServiceUnavailable) return } hits, err := s.Search(r.Context(), s.owner(r), r.URL.Query().Get("q")) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, hits) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }