Files
ragflow/internal/cli/filesystem/skill_hub/source/interface.go
Yingfeng 4ee0702aed Feat: add skills space to context engine (#13908)
### What problem does this PR solve?

issue #13714

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-04-30 12:36:03 +08:00

178 lines
5.4 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package source
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
// SkillSource is the interface for skill sources
type SkillSource interface {
// SourceID returns the source identifier (local, github, clawhub, skillssh)
SourceID() string
// Fetch downloads and returns the skill bundle
Fetch(identifier string) (*SkillBundle, error)
// Inspect retrieves metadata without downloading full content
Inspect(identifier string) (*SkillMetadata, error)
// TrustLevel returns the trust level for this source (builtin/trusted/community)
TrustLevel(identifier string) string
}
// SourceResolver resolves source references to appropriate adapters
type SourceResolver struct {
sources map[string]SkillSource
}
// NewSourceResolver creates a new source resolver
func NewSourceResolver(client HTTPClientInterface) *SourceResolver {
return &SourceResolver{
sources: map[string]SkillSource{
"local": NewLocalSource(),
"github": NewGitHubSource(client),
"clawhub": NewClawHubSource(client),
"skillssh": NewSkillsShSource(client),
},
}
}
// Resolve parses a source reference and returns the appropriate source adapter
// Supported formats:
// - ./path, /absolute/path -> local
// - github.com/owner/repo/path -> github
// - clawhub://owner/skill-name, clawhub.ai/owner/skill-name -> clawhub
// - skill://skill-name, skills.sh/skill/name -> skillssh
func (r *SourceResolver) Resolve(ref string) (SkillSource, string, error) {
ref = strings.TrimSpace(ref)
if ref == "" {
return nil, "", fmt.Errorf("empty source reference")
}
// Check for URI schemes
if strings.HasPrefix(ref, "clawhub://") {
identifier := strings.TrimPrefix(ref, "clawhub://")
return r.sources["clawhub"], identifier, nil
}
if strings.HasPrefix(ref, "skill://") {
identifier := strings.TrimPrefix(ref, "skill://")
return r.sources["skillssh"], identifier, nil
}
// Check for local path (starts with ./ or / or ~)
if strings.HasPrefix(ref, "./") || strings.HasPrefix(ref, "/") || strings.HasPrefix(ref, "~/") {
// Expand ~ to home directory
if strings.HasPrefix(ref, "~/") {
home, err := getHomeDir()
if err != nil {
return nil, "", fmt.Errorf("cannot resolve home directory: %w", err)
}
ref = filepath.Join(home, ref[2:])
}
return r.sources["local"], ref, nil
}
// Check for github.com domain
if strings.HasPrefix(ref, "github.com/") || strings.HasPrefix(ref, "https://github.com/") {
identifier := strings.TrimPrefix(ref, "https://")
return r.sources["github"], identifier, nil
}
// Check for clawhub.ai domain
if strings.HasPrefix(ref, "clawhub.ai/") || strings.HasPrefix(ref, "https://clawhub.ai/") {
identifier := strings.TrimPrefix(ref, "https://")
identifier = strings.TrimPrefix(identifier, "clawhub.ai/")
return r.sources["clawhub"], identifier, nil
}
// Check for skills.sh domain
if strings.HasPrefix(ref, "skills.sh/") || strings.HasPrefix(ref, "https://skills.sh/") {
identifier := strings.TrimPrefix(ref, "https://")
identifier = strings.TrimPrefix(identifier, "skills.sh/")
return r.sources["skillssh"], identifier, nil
}
// Default: treat as local path if it exists, otherwise error
return r.sources["local"], ref, nil
}
// getHomeDir returns the user's home directory
func getHomeDir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
return "", fmt.Errorf("cannot determine home directory")
}
return home, nil
}
// parseGitHubURL parses a GitHub URL and returns owner, repo, and path
func parseGitHubURL(urlStr string) (owner, repo, path string, err error) {
// Remove protocol prefix if present
urlStr = strings.TrimPrefix(urlStr, "https://")
urlStr = strings.TrimPrefix(urlStr, "http://")
// Remove github.com/ prefix
urlStr = strings.TrimPrefix(urlStr, "github.com/")
parts := strings.Split(urlStr, "/")
if len(parts) < 2 {
return "", "", "", fmt.Errorf("invalid GitHub URL format")
}
owner = parts[0]
repo = parts[1]
if len(parts) > 2 {
path = strings.Join(parts[2:], "/")
}
return owner, repo, path, nil
}
// extractSkillNameFromPath extracts the skill name from a path
func extractSkillNameFromPath(path string) string {
base := filepath.Base(path)
// Remove common suffixes
base = strings.TrimSuffix(base, ".git")
return base
}
// isTrustedGitHubRepo checks if a GitHub repo is trusted
func isTrustedGitHubRepo(owner, repo string) bool {
fullName := owner + "/" + repo
trusted := map[string]bool{
"openai/skills": true,
"anthropics/skills": true,
"microsoft/skills": true,
"google/skills": true,
}
return trusted[fullName]
}
// Helper to check if URL is valid
func isValidURL(str string) bool {
u, err := url.Parse(str)
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
}