feat: 自托管 AI Gateway 运维管理 SKILL
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
gatewaydocs "github.com/easyai/easyai-ai-gateway/apps/api/docs"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/skillbundle"
|
||||
)
|
||||
|
||||
const (
|
||||
opsManagementSkillDownloadPath = "/api/v1/public/skills/ai-gateway-ops-management/download"
|
||||
apiDocsJSONPath = "/api-docs-json"
|
||||
apiDocsYAMLPath = "/api-docs-yaml"
|
||||
)
|
||||
|
||||
// getOpsManagementSkillMetadata godoc
|
||||
// @Summary 获取 AI Gateway 运维管理 SKILL 元数据
|
||||
// @Description 返回公开运维管理 SKILL 的名称、版本、模块、下载文件名和机器可读接口文档路径。
|
||||
// @Tags agent-resources
|
||||
// @Produce json
|
||||
// @Success 200 {object} SkillBundleMetadataResponse
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/v1/public/skills/ai-gateway-ops-management/metadata [get]
|
||||
func (s *Server) getOpsManagementSkillMetadata(w http.ResponseWriter, _ *http.Request) {
|
||||
metadata, err := skillbundle.LoadMetadata()
|
||||
if err != nil {
|
||||
s.logger.Error("load operations skill metadata failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "operations skill metadata unavailable")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, opsManagementSkillMetadataResponse(metadata))
|
||||
}
|
||||
|
||||
// downloadOpsManagementSkill godoc
|
||||
// @Summary 下载 AI Gateway 运维管理 SKILL
|
||||
// @Description 下载可交给 Agent 使用的 ai-gateway-ops-management ZIP 包。
|
||||
// @Tags agent-resources
|
||||
// @Produce application/zip
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/v1/public/skills/ai-gateway-ops-management/download [get]
|
||||
func (s *Server) downloadOpsManagementSkill(w http.ResponseWriter, _ *http.Request) {
|
||||
metadata, err := skillbundle.LoadMetadata()
|
||||
if err != nil {
|
||||
s.logger.Error("load operations skill metadata failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "operations skill metadata unavailable")
|
||||
return
|
||||
}
|
||||
archive, err := skillbundle.BuildArchive()
|
||||
if err != nil {
|
||||
s.logger.Error("build operations skill archive failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "operations skill download unavailable")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+skillbundle.FileName(metadata)+`"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(archive)
|
||||
}
|
||||
|
||||
// apiDocsJSON godoc
|
||||
// @Summary 获取 AI Gateway Swagger JSON
|
||||
// @Description 返回当前构建内嵌的完整机器可读 Swagger JSON,供 Agent 在 SKILL references 未覆盖接口时查询。
|
||||
// @Tags agent-resources
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api-docs-json [get]
|
||||
func (s *Server) apiDocsJSON(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(gatewaydocs.SwaggerJSON)
|
||||
}
|
||||
|
||||
// apiDocsYAML godoc
|
||||
// @Summary 获取 AI Gateway Swagger YAML
|
||||
// @Description 返回当前构建内嵌的完整机器可读 Swagger YAML。
|
||||
// @Tags agent-resources
|
||||
// @Produce application/yaml
|
||||
// @Success 200 {string} string
|
||||
// @Router /api-docs-yaml [get]
|
||||
func (s *Server) apiDocsYAML(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(gatewaydocs.SwaggerYAML)
|
||||
}
|
||||
|
||||
func opsManagementSkillMetadataResponse(metadata skillbundle.Metadata) SkillBundleMetadataResponse {
|
||||
return SkillBundleMetadataResponse{
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
DisplayName: skillbundle.DisplayName,
|
||||
Modules: metadata.Modules,
|
||||
FileName: skillbundle.FileName(metadata),
|
||||
DownloadPath: opsManagementSkillDownloadPath,
|
||||
APIDocsJSONPath: apiDocsJSONPath,
|
||||
APIDocsYAMLPath: apiDocsYAMLPath,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetOpsManagementSkillMetadata(t *testing.T) {
|
||||
server := &Server{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/public/skills/ai-gateway-ops-management/metadata", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.getOpsManagementSkillMetadata(response, request)
|
||||
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("expected metadata status 200, got %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
var metadata SkillBundleMetadataResponse
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &metadata); err != nil {
|
||||
t.Fatalf("decode metadata: %v", err)
|
||||
}
|
||||
if metadata.Name != "ai-gateway-ops-management" || metadata.Version != "1.0.1" {
|
||||
t.Fatalf("unexpected metadata: %+v", metadata)
|
||||
}
|
||||
if len(metadata.Modules) != 1 || metadata.Modules[0] != "model-runtime" {
|
||||
t.Fatalf("unexpected metadata modules: %+v", metadata.Modules)
|
||||
}
|
||||
if metadata.APIDocsJSONPath != "/api-docs-json" || metadata.APIDocsYAMLPath != "/api-docs-yaml" {
|
||||
t.Fatalf("unexpected API docs paths: %+v", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadOpsManagementSkill(t *testing.T) {
|
||||
server := &Server{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/public/skills/ai-gateway-ops-management/download", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
server.downloadOpsManagementSkill(response, request)
|
||||
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("expected download status 200, got %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
if response.Header().Get("Content-Type") != "application/zip" {
|
||||
t.Fatalf("unexpected content type: %q", response.Header().Get("Content-Type"))
|
||||
}
|
||||
if disposition := response.Header().Get("Content-Disposition"); !strings.Contains(disposition, "ai-gateway-ops-management-v1.0.1.zip") {
|
||||
t.Fatalf("unexpected content disposition: %q", disposition)
|
||||
}
|
||||
raw := response.Body.Bytes()
|
||||
archive, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
|
||||
if err != nil {
|
||||
t.Fatalf("open downloaded archive: %v", err)
|
||||
}
|
||||
foundSkill := false
|
||||
for _, file := range archive.File {
|
||||
if file.Name == "SKILL.md" {
|
||||
foundSkill = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundSkill {
|
||||
t.Fatalf("downloaded archive does not contain SKILL.md")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedAPIDocs(t *testing.T) {
|
||||
server := &Server{}
|
||||
|
||||
jsonResponse := httptest.NewRecorder()
|
||||
server.apiDocsJSON(jsonResponse, httptest.NewRequest(http.MethodGet, "/api-docs-json", nil))
|
||||
if jsonResponse.Code != http.StatusOK {
|
||||
t.Fatalf("expected JSON docs status 200, got %d", jsonResponse.Code)
|
||||
}
|
||||
if jsonResponse.Header().Get("Content-Type") != "application/json; charset=utf-8" {
|
||||
t.Fatalf("unexpected JSON docs content type: %q", jsonResponse.Header().Get("Content-Type"))
|
||||
}
|
||||
var document struct {
|
||||
Paths map[string]json.RawMessage `json:"paths"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonResponse.Body.Bytes(), &document); err != nil {
|
||||
t.Fatalf("decode embedded Swagger JSON: %v", err)
|
||||
}
|
||||
for _, path := range []string{
|
||||
"/api-docs-json",
|
||||
"/api/v1/public/skills/ai-gateway-ops-management/download",
|
||||
"/api/admin/catalog/providers",
|
||||
"/api/admin/catalog/base-models",
|
||||
"/api/admin/platforms",
|
||||
"/api/admin/runtime/policy-sets",
|
||||
"/api/admin/pricing/rule-sets",
|
||||
} {
|
||||
if document.Paths[path] == nil {
|
||||
t.Fatalf("embedded Swagger JSON missing %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
yamlResponse := httptest.NewRecorder()
|
||||
server.apiDocsYAML(yamlResponse, httptest.NewRequest(http.MethodGet, "/api-docs-yaml", nil))
|
||||
if yamlResponse.Code != http.StatusOK {
|
||||
t.Fatalf("expected YAML docs status 200, got %d", yamlResponse.Code)
|
||||
}
|
||||
if yamlResponse.Header().Get("Content-Type") != "application/yaml; charset=utf-8" {
|
||||
t.Fatalf("unexpected YAML docs content type: %q", yamlResponse.Header().Get("Content-Type"))
|
||||
}
|
||||
if !strings.Contains(yamlResponse.Body.String(), "/api/v1/public/skills/ai-gateway-ops-management/metadata") {
|
||||
t.Fatalf("embedded Swagger YAML missing operations skill metadata path")
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,17 @@ type ReadyResponse struct {
|
||||
OK bool `json:"ok" example:"true"`
|
||||
}
|
||||
|
||||
type SkillBundleMetadataResponse struct {
|
||||
Name string `json:"name" example:"ai-gateway-ops-management"`
|
||||
Version string `json:"version" example:"1.0.1"`
|
||||
DisplayName string `json:"displayName" example:"AI Gateway 运维管理"`
|
||||
Modules []string `json:"modules" example:"model-runtime"`
|
||||
FileName string `json:"fileName" example:"ai-gateway-ops-management-v1.0.1.zip"`
|
||||
DownloadPath string `json:"downloadPath" example:"/api/v1/public/skills/ai-gateway-ops-management/download"`
|
||||
APIDocsJSONPath string `json:"apiDocsJsonPath" example:"/api-docs-json"`
|
||||
APIDocsYAMLPath string `json:"apiDocsYamlPath" example:"/api-docs-yaml"`
|
||||
}
|
||||
|
||||
type ErrorEnvelope struct {
|
||||
Error ErrorPayload `json:"error"`
|
||||
}
|
||||
|
||||
@@ -121,6 +121,10 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset)
|
||||
mux.HandleFunc("GET /static/generated/{asset}", server.serveGeneratedStaticAsset)
|
||||
mux.HandleFunc("GET /static/uploaded/{asset}", server.serveUploadedStaticAsset)
|
||||
mux.HandleFunc("GET /api-docs-json", server.apiDocsJSON)
|
||||
mux.HandleFunc("GET /api-docs-yaml", server.apiDocsYAML)
|
||||
mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/metadata", server.getOpsManagementSkillMetadata)
|
||||
mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/download", server.downloadOpsManagementSkill)
|
||||
|
||||
mux.Handle("POST /api/v1/auth/register", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.register)))
|
||||
mux.Handle("POST /api/v1/auth/login", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.login)))
|
||||
|
||||
Reference in New Issue
Block a user