package workbench import ( "net/http" "strconv" "strings" "time" "aigateway.local/core/internal/identity" "aigateway.local/core/internal/platform/apiresponse" ) // MarketplaceAdminHTTPHandler exposes the resource-marketplace administration // surface: shared categories, MCP servers (with connectivity test), skills, // digital employees, and the unified catalog. type MarketplaceAdminHTTPHandler struct { market *MarketplaceService mcpServers *MCPServerService skills *SkillService employees *DigitalEmployeeService mcpClient *MCPClient identity *identity.Service mux *http.ServeMux } func NewMarketplaceAdminHTTPHandler(market *MarketplaceService, mcpServers *MCPServerService, skills *SkillService, employees *DigitalEmployeeService, mcpClient *MCPClient, identityService *identity.Service) *MarketplaceAdminHTTPHandler { h := &MarketplaceAdminHTTPHandler{market: market, mcpServers: mcpServers, skills: skills, employees: employees, mcpClient: mcpClient, identity: identityService, mux: http.NewServeMux()} h.mux.HandleFunc("GET /api/v1/admin/marketplace-categories", h.listMarketplaceCategories) h.mux.HandleFunc("POST /api/v1/admin/marketplace-categories", h.createMarketplaceCategory) h.mux.HandleFunc("DELETE /api/v1/admin/marketplace-categories/{id}", h.deleteMarketplaceCategory) h.mux.HandleFunc("GET /api/v1/admin/mcp-servers", h.listMCPServers) h.mux.HandleFunc("POST /api/v1/admin/mcp-servers", h.createMCPServer) h.mux.HandleFunc("GET /api/v1/admin/mcp-servers/{id}", h.getMCPServer) h.mux.HandleFunc("PUT /api/v1/admin/mcp-servers/{id}", h.updateMCPServer) h.mux.HandleFunc("DELETE /api/v1/admin/mcp-servers/{id}", h.deleteMCPServer) h.mux.HandleFunc("POST /api/v1/admin/mcp-servers/{id}/test", h.testMCPServer) h.mux.HandleFunc("POST /api/v1/admin/mcp-servers/{id}/scan", h.scanMCP) h.mux.HandleFunc("POST /api/v1/admin/skills/{id}/scan", h.scanSkill) h.mux.HandleFunc("GET /api/v1/admin/skills", h.listSkills) h.mux.HandleFunc("POST /api/v1/admin/skills", h.createSkill) h.mux.HandleFunc("GET /api/v1/admin/skills/{id}", h.getSkill) h.mux.HandleFunc("PUT /api/v1/admin/skills/{id}", h.updateSkill) h.mux.HandleFunc("DELETE /api/v1/admin/skills/{id}", h.deleteSkill) h.mux.HandleFunc("POST /api/v1/admin/skills/{id}/render", h.renderSkillPreview) h.mux.HandleFunc("GET /api/v1/admin/digital-employees", h.listDigitalEmployees) h.mux.HandleFunc("POST /api/v1/admin/digital-employees", h.createDigitalEmployee) h.mux.HandleFunc("GET /api/v1/admin/digital-employees/{id}", h.getDigitalEmployee) h.mux.HandleFunc("PUT /api/v1/admin/digital-employees/{id}", h.updateDigitalEmployee) h.mux.HandleFunc("DELETE /api/v1/admin/digital-employees/{id}", h.deleteDigitalEmployee) h.mux.HandleFunc("POST /api/v1/admin/digital-employees/{id}/publish", h.publishDigitalEmployee) h.mux.HandleFunc("POST /api/v1/admin/digital-employees/{id}/archive", h.archiveDigitalEmployee) h.mux.HandleFunc("GET /api/v1/admin/digital-employees/{id}/runs", h.listDigitalEmployeeRuns) h.mux.HandleFunc("GET /api/v1/admin/marketplace/catalog", h.marketplaceCatalog) return h } func (h *MarketplaceAdminHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } func (h *MarketplaceAdminHTTPHandler) require(w http.ResponseWriter, r *http.Request, permission string) (identity.Account, bool) { account, err := h.identity.Authenticate(r.Context(), identity.KindAdmin, r.Header.Get("Authorization")) if err != nil { apiresponse.Error(w, 401, "登录状态无效") return account, false } if !identity.HasPermission(account, permission) { apiresponse.Error(w, 403, "缺少资源市场管理权限") return account, false } return account, true } // --- 分类 --- func (h *MarketplaceAdminHTTPHandler) listMarketplaceCategories(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionMarketplaceRead); !ok { return } items, err := h.market.ListCategories(r.Context(), r.URL.Query().Get("type")) if err != nil { assetError(w, err) return } apiresponse.OK(w, items) } func (h *MarketplaceAdminHTTPHandler) createMarketplaceCategory(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionMarketplaceManage) if !ok { return } var input struct { Name string `json:"name"` Description string `json:"description"` ResourceType string `json:"resource_type"` SortOrder int `json:"sort_order"` } if !decodeAsset(w, r, &input) { return } item, err := h.market.CreateCategory(r.Context(), Category{Name: input.Name, Description: input.Description, ResourceType: input.ResourceType, SortOrder: input.SortOrder}, a.ID) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) deleteMarketplaceCategory(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionMarketplaceManage) if !ok { return } if err := h.market.DeleteCategory(r.Context(), r.PathValue("id"), a.ID); err != nil { assetError(w, err) return } apiresponse.OK(w, map[string]bool{"deleted": true}) } // --- MCP 服务器 --- func (h *MarketplaceAdminHTTPHandler) listMCPServers(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionMCPServerRead); !ok { return } items, err := h.mcpServers.List(r.Context()) if err != nil { assetError(w, err) return } apiresponse.OK(w, items) } func (h *MarketplaceAdminHTTPHandler) createMCPServer(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionMCPServerManage) if !ok { return } var input MCPServerInput if !decodeAsset(w, r, &input) { return } item, err := h.mcpServers.Save(r.Context(), "", input, a.ID, true) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) getMCPServer(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionMCPServerRead); !ok { return } item, err := h.mcpServers.Get(r.Context(), r.PathValue("id")) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) updateMCPServer(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionMCPServerManage) if !ok { return } var input MCPServerInput if !decodeAsset(w, r, &input) { return } item, err := h.mcpServers.Save(r.Context(), r.PathValue("id"), input, a.ID, false) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) deleteMCPServer(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionMCPServerManage) if !ok { return } if err := h.mcpServers.Delete(r.Context(), r.PathValue("id"), a.ID); err != nil { assetError(w, err) return } apiresponse.OK(w, map[string]bool{"deleted": true}) } // testMCPServer verifies connectivity and lists the tools an MCP server // advertises so an operator can confirm it before publishing. func (h *MarketplaceAdminHTTPHandler) testMCPServer(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionMCPServerManage); !ok { return } server, err := h.mcpServers.Get(r.Context(), r.PathValue("id")) if err != nil { assetError(w, err) return } headers, err := h.mcpServers.Headers(server) if err != nil { assetError(w, err) return } tools, err := h.mcpClient.DiscoverTools(r.Context(), server, headers) if err != nil { apiresponse.Error(w, 502, "MCP 服务器连通性测试失败: "+err.Error()) return } apiresponse.OK(w, tools) } // --- Skills --- func (h *MarketplaceAdminHTTPHandler) listSkills(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionSkillRead); !ok { return } items, err := h.skills.List(r.Context()) if err != nil { assetError(w, err) return } apiresponse.OK(w, items) } func (h *MarketplaceAdminHTTPHandler) createSkill(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionSkillManage) if !ok { return } var input SkillInput if !decodeAsset(w, r, &input) { return } item, err := h.skills.Save(r.Context(), "", input, a.ID, true) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) getSkill(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionSkillRead); !ok { return } item, err := h.skills.Get(r.Context(), r.PathValue("id")) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) updateSkill(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionSkillManage) if !ok { return } var input SkillInput if !decodeAsset(w, r, &input) { return } item, err := h.skills.Save(r.Context(), r.PathValue("id"), input, a.ID, false) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) deleteSkill(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionSkillManage) if !ok { return } if err := h.skills.Delete(r.Context(), r.PathValue("id"), a.ID); err != nil { assetError(w, err) return } apiresponse.OK(w, map[string]bool{"deleted": true}) } func (h *MarketplaceAdminHTTPHandler) renderSkillPreview(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionSkillRead); !ok { return } var input struct { Variables map[string]any `json:"variables"` } if !decodeAsset(w, r, &input) { return } skill, err := h.skills.Get(r.Context(), r.PathValue("id")) if err != nil { assetError(w, err) return } rendered, err := h.skills.Render(skill, input.Variables) if err != nil { apiresponse.Error(w, 400, err.Error()) return } apiresponse.OK(w, map[string]any{"code": skill.Code, "name": skill.Name, "rendered": rendered}) } // --- 数字员工 --- func (h *MarketplaceAdminHTTPHandler) listDigitalEmployees(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionDigitalEmployeeRead); !ok { return } items, err := h.employees.List(r.Context()) if err != nil { assetError(w, err) return } apiresponse.OK(w, items) } func (h *MarketplaceAdminHTTPHandler) createDigitalEmployee(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionDigitalEmployeeManage) if !ok { return } var input DigitalEmployeeInput if !decodeAsset(w, r, &input) { return } item, err := h.employees.Save(r.Context(), "", input, a.ID, true) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) getDigitalEmployee(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionDigitalEmployeeRead); !ok { return } item, err := h.employees.Get(r.Context(), r.PathValue("id")) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) updateDigitalEmployee(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionDigitalEmployeeManage) if !ok { return } var input DigitalEmployeeInput if !decodeAsset(w, r, &input) { return } item, err := h.employees.Save(r.Context(), r.PathValue("id"), input, a.ID, false) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) deleteDigitalEmployee(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionDigitalEmployeeManage) if !ok { return } if err := h.employees.Delete(r.Context(), r.PathValue("id"), a.ID); err != nil { assetError(w, err) return } apiresponse.OK(w, map[string]bool{"deleted": true}) } func (h *MarketplaceAdminHTTPHandler) publishDigitalEmployee(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionDigitalEmployeeManage) if !ok { return } item, err := h.employees.Publish(r.Context(), r.PathValue("id"), a.ID) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) archiveDigitalEmployee(w http.ResponseWriter, r *http.Request) { a, ok := h.require(w, r, identity.PermissionDigitalEmployeeManage) if !ok { return } item, err := h.employees.Archive(r.Context(), r.PathValue("id"), a.ID) if err != nil { assetError(w, err) return } apiresponse.OK(w, item) } func (h *MarketplaceAdminHTTPHandler) listDigitalEmployeeRuns(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionDigitalEmployeeRead); !ok { return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) items, err := h.employees.ListRuns(r.Context(), r.PathValue("id"), limit) if err != nil { assetError(w, err) return } apiresponse.OK(w, items) } // --- 统一目录 --- func (h *MarketplaceAdminHTTPHandler) marketplaceCatalog(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionMarketplaceRead); !ok { return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) query := r.URL.Query() items, err := h.market.Catalog(r.Context(), query.Get("type"), query.Get("category_id"), query.Get("tag"), query.Get("q"), limit) if err != nil { assetError(w, err) return } if items == nil { items = []MarketItem{} } apiresponse.OK(w, items) } // scanMCP 对 MCP 服务器定义执行供应链静态扫描。 func (h *MarketplaceAdminHTTPHandler) scanMCP(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionMCPServerManage); !ok { return } server, err := h.mcpServers.Get(r.Context(), r.PathValue("id")) if err != nil { apiresponse.Error(w, http.StatusNotFound, "MCP 服务器不存在") return } content := strings.Join([]string{server.Name, server.Description, server.EndpointURL}, "\n") if len(server.EncryptedHeaders) > 0 { content += "\n[配置了加密请求头]" } apiresponse.OK(w, map[string]any{ "findings": ScanResource(content), "highest": HighestSeverity(ScanResource(content)), "scanned_at": time.Now().UTC(), "resource": server.Name, }) } // scanSkill 对 Skill 定义执行供应链静态扫描。 func (h *MarketplaceAdminHTTPHandler) scanSkill(w http.ResponseWriter, r *http.Request) { if _, ok := h.require(w, r, identity.PermissionSkillManage); !ok { return } skill, err := h.skills.Get(r.Context(), r.PathValue("id")) if err != nil { apiresponse.Error(w, http.StatusNotFound, "Skill 不存在") return } content := strings.Join([]string{skill.Name, skill.Description, skill.Content}, "\n") apiresponse.OK(w, map[string]any{ "findings": ScanResource(content), "highest": HighestSeverity(ScanResource(content)), "scanned_at": time.Now().UTC(), "resource": skill.Name, }) }