5759c1862e
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
436 lines
13 KiB
Go
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)
|
|
}
|