Files
ai-gateway-go/internal/workbench/admin_marketplace.go
T
superidou 5759c1862e AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。
含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 11:45:54 +08:00

436 lines
13 KiB
Go

package workbench
import (
"net/http"
"strconv"
"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("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)
}