easyai-ai-gateway/apps/api/internal/skillbundle/bundle.go

123 lines
3.6 KiB
Go

package skillbundle
import (
"archive/zip"
"bytes"
"embed"
"encoding/json"
"fmt"
"io/fs"
"path"
"regexp"
"sort"
"strings"
"time"
)
const (
Name = "ai-gateway-ops-management"
DisplayName = "AI Gateway 运维管理"
)
const bundleRoot = "content/skills/" + Name
var semverPattern = regexp.MustCompile(`^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`)
//go:embed content/skills/ai-gateway-ops-management
var bundleFS embed.FS
type Metadata struct {
Name string `json:"name"`
Version string `json:"version"`
Modules []string `json:"modules"`
}
func LoadMetadata() (Metadata, error) {
raw, err := bundleFS.ReadFile(bundleRoot + "/skill.json")
if err != nil {
return Metadata{}, fmt.Errorf("read bundled skill metadata: %w", err)
}
var metadata Metadata
if err := json.Unmarshal(raw, &metadata); err != nil {
return Metadata{}, fmt.Errorf("parse bundled skill metadata: %w", err)
}
if metadata.Name != Name {
return Metadata{}, fmt.Errorf("bundled skill metadata name must be %q", Name)
}
if !semverPattern.MatchString(metadata.Version) {
return Metadata{}, fmt.Errorf("bundled skill metadata version %q is not semver", metadata.Version)
}
if len(metadata.Modules) == 0 {
return Metadata{}, fmt.Errorf("bundled skill metadata modules must not be empty")
}
seen := make(map[string]struct{}, len(metadata.Modules))
for _, module := range metadata.Modules {
module = strings.TrimSpace(module)
if module == "" {
return Metadata{}, fmt.Errorf("bundled skill metadata module must not be empty")
}
if _, exists := seen[module]; exists {
return Metadata{}, fmt.Errorf("bundled skill metadata module %q is duplicated", module)
}
seen[module] = struct{}{}
}
if _, err := bundleFS.ReadFile(bundleRoot + "/SKILL.md"); err != nil {
return Metadata{}, fmt.Errorf("read bundled SKILL.md: %w", err)
}
return metadata, nil
}
func FileName(metadata Metadata) string {
return fmt.Sprintf("%s-v%s.zip", metadata.Name, metadata.Version)
}
func BuildArchive() ([]byte, error) {
if _, err := LoadMetadata(); err != nil {
return nil, err
}
files := make([]string, 0)
if err := fs.WalkDir(bundleFS, bundleRoot, func(filePath string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
return nil
}
relativePath := strings.TrimPrefix(filePath, bundleRoot+"/")
if relativePath == "" || path.IsAbs(relativePath) || path.Clean(relativePath) != relativePath || strings.HasPrefix(relativePath, "../") {
return fmt.Errorf("unsafe bundled skill path %q", relativePath)
}
files = append(files, relativePath)
return nil
}); err != nil {
return nil, fmt.Errorf("walk bundled skill: %w", err)
}
sort.Strings(files)
var output bytes.Buffer
archive := zip.NewWriter(&output)
fixedTime := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
for _, relativePath := range files {
raw, err := bundleFS.ReadFile(bundleRoot + "/" + relativePath)
if err != nil {
_ = archive.Close()
return nil, fmt.Errorf("read bundled skill file %q: %w", relativePath, err)
}
header := &zip.FileHeader{Name: relativePath, Method: zip.Deflate}
header.SetModTime(fixedTime)
writer, err := archive.CreateHeader(header)
if err != nil {
_ = archive.Close()
return nil, fmt.Errorf("create bundled skill archive entry %q: %w", relativePath, err)
}
if _, err := writer.Write(raw); err != nil {
_ = archive.Close()
return nil, fmt.Errorf("write bundled skill archive entry %q: %w", relativePath, err)
}
}
if err := archive.Close(); err != nil {
return nil, fmt.Errorf("close bundled skill archive: %w", err)
}
return output.Bytes(), nil
}