123 lines
3.6 KiB
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
|
|
}
|