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 }