mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-01 00:05:43 +08:00
### What problem does this PR solve? issue #13714 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
207 lines
5.4 KiB
Go
207 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"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// LocalSource handles local filesystem skills
|
|
type LocalSource struct{}
|
|
|
|
// NewLocalSource creates a new local source adapter
|
|
func NewLocalSource() *LocalSource {
|
|
return &LocalSource{}
|
|
}
|
|
|
|
// SourceID returns the source identifier
|
|
func (s *LocalSource) SourceID() string {
|
|
return "local"
|
|
}
|
|
|
|
// TrustLevel returns the trust level for local sources
|
|
func (s *LocalSource) TrustLevel(identifier string) string {
|
|
return "community" // Local skills default to community trust level
|
|
}
|
|
|
|
// Fetch retrieves a skill from the local filesystem
|
|
func (s *LocalSource) Fetch(identifier string) (*SkillBundle, error) {
|
|
// Validate path exists
|
|
info, err := os.Stat(identifier)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot access path %s: %w", identifier, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return nil, fmt.Errorf("%s is not a directory", identifier)
|
|
}
|
|
|
|
// Read SKILL.md
|
|
skillMdPath := filepath.Join(identifier, "SKILL.md")
|
|
content, err := os.ReadFile(skillMdPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SKILL.md not found in %s: %w", identifier, err)
|
|
}
|
|
|
|
// Parse frontmatter
|
|
meta, err := parseSkillFrontmatter(string(content))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SKILL.md frontmatter in %s: %w", identifier, err)
|
|
}
|
|
skillName := meta.Name
|
|
if skillName == "" {
|
|
skillName = filepath.Base(identifier)
|
|
}
|
|
|
|
// Collect all files
|
|
files := make(map[string][]byte)
|
|
ignorePatterns := []string{
|
|
".git/", ".svn/", ".hg/", "node_modules/", "__MACOSX/",
|
|
".DS_Store", "._*", "*.log", "*.tmp", "*.temp", "*.swp", "*.swo", "*~",
|
|
".env", ".env.*", ".vscode/", ".idea/", "Thumbs.db", "desktop.ini",
|
|
}
|
|
|
|
err = filepath.Walk(identifier, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// Skip non-regular files (symlinks, devices, pipes, etc.)
|
|
if !info.Mode().IsRegular() {
|
|
return nil
|
|
}
|
|
|
|
relPath, err := filepath.Rel(identifier, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check ignore patterns
|
|
for _, pattern := range ignorePatterns {
|
|
if matched, _ := filepath.Match(pattern, relPath); matched {
|
|
return nil
|
|
}
|
|
if strings.Contains(relPath, pattern) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Only include text files based on extension
|
|
if !isTextFile(path) {
|
|
return nil
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files[relPath] = data
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &SkillBundle{
|
|
Name: skillName,
|
|
Files: files,
|
|
Source: "local",
|
|
Identifier: identifier,
|
|
TrustLevel: s.TrustLevel(identifier),
|
|
Metadata: meta,
|
|
}, nil
|
|
}
|
|
|
|
// Inspect retrieves metadata without reading all files
|
|
func (s *LocalSource) Inspect(identifier string) (*SkillMetadata, error) {
|
|
info, err := os.Stat(identifier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !info.IsDir() {
|
|
return nil, fmt.Errorf("not a directory")
|
|
}
|
|
|
|
skillMdPath := filepath.Join(identifier, "SKILL.md")
|
|
content, err := os.ReadFile(skillMdPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
meta, err := parseSkillFrontmatter(string(content))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SKILL.md frontmatter in %s: %w", identifier, err)
|
|
}
|
|
if meta.Name == "" {
|
|
meta.Name = filepath.Base(identifier)
|
|
}
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// parseSkillFrontmatter extracts YAML frontmatter from SKILL.md content
|
|
// Returns an error if frontmatter delimiters are missing or YAML is invalid
|
|
func parseSkillFrontmatter(content string) (*SkillMetadata, error) {
|
|
meta := &SkillMetadata{}
|
|
|
|
// Look for YAML frontmatter
|
|
content = strings.TrimSpace(content)
|
|
if !strings.HasPrefix(content, "---") {
|
|
return nil, fmt.Errorf("missing opening frontmatter delimiter '---'")
|
|
}
|
|
|
|
// Find end of frontmatter
|
|
endIdx := strings.Index(content[3:], "---")
|
|
if endIdx == -1 {
|
|
return nil, fmt.Errorf("missing closing frontmatter delimiter '---'")
|
|
}
|
|
|
|
frontmatter := content[3 : endIdx+3]
|
|
if err := yaml.Unmarshal([]byte(frontmatter), meta); err != nil {
|
|
return nil, fmt.Errorf("invalid YAML frontmatter: %w", err)
|
|
}
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// isTextFile checks if a file is a text file based on extension
|
|
func isTextFile(filename string) bool {
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
if ext != "" && ext[0] == '.' {
|
|
ext = ext[1:]
|
|
}
|
|
|
|
textExts := map[string]bool{
|
|
"md": true, "mdx": true, "txt": true, "json": true, "json5": true,
|
|
"yaml": true, "yml": true, "toml": true, "js": true, "cjs": true, "mjs": true,
|
|
"ts": true, "tsx": true, "jsx": true, "py": true, "sh": true, "rb": true,
|
|
"go": true, "rs": true, "swift": true, "kt": true, "java": true, "cs": true,
|
|
"cpp": true, "c": true, "h": true, "hpp": true, "sql": true, "csv": true,
|
|
"ini": true, "cfg": true, "env": true, "xml": true, "html": true,
|
|
"css": true, "scss": true, "sass": true, "svg": true,
|
|
}
|
|
|
|
return textExts[ext]
|
|
}
|