Files
ai-gateway-go/internal/workbench/admin_marketplace.go
T
superidou c22669c31d 0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆)
- 新增包: license/memory/modelquota/assistant,扫描引擎
- 全部功能后端+前端+端到端验证通过(25 包单测)
2026-08-13 11:37:18 +08:00

482 lines
15 KiB
Go

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,
})
}