mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
206 lines
4.7 KiB
Go
206 lines
4.7 KiB
Go
package files
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type FileAIContentHit struct {
|
|
Path string `json:"path"`
|
|
Line int `json:"line"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
var fileAIBinaryExt = map[string]struct{}{
|
|
".png": {}, ".jpg": {}, ".jpeg": {}, ".gif": {}, ".webp": {}, ".ico": {}, ".bmp": {},
|
|
".zip": {}, ".gz": {}, ".tgz": {}, ".bz2": {}, ".xz": {}, ".7z": {}, ".rar": {},
|
|
".exe": {}, ".dll": {}, ".so": {}, ".dylib": {}, ".bin": {},
|
|
".pdf": {}, ".doc": {}, ".docx": {}, ".xls": {}, ".xlsx": {},
|
|
".mp3": {}, ".mp4": {}, ".avi": {}, ".mkv": {}, ".mov": {},
|
|
".woff": {}, ".woff2": {}, ".ttf": {}, ".eot": {},
|
|
}
|
|
|
|
func SearchFileAIContentHits(root string, inventory []AISearchInventoryItem, opts ContentSearchOptions, match func(string) bool) (hits []FileAIContentHit, scannedFiles int, scanTruncated bool) {
|
|
if len(inventory) == 0 || match == nil {
|
|
return nil, 0, false
|
|
}
|
|
root = filepath.Clean(root)
|
|
maxFile := opts.MaxFileBytes
|
|
if maxFile <= 0 {
|
|
maxFile = int64(fileAIDefaultMaxFileBytes)
|
|
}
|
|
if maxFile > int64(fileAIHardMaxFileBytes) {
|
|
maxFile = int64(fileAIHardMaxFileBytes)
|
|
}
|
|
maxScan := opts.MaxScanFiles
|
|
if maxScan <= 0 {
|
|
maxScan = fileAIDefaultMaxScanFiles
|
|
}
|
|
if maxScan > fileAIHardMaxScanFiles {
|
|
maxScan = fileAIHardMaxScanFiles
|
|
}
|
|
maxPerFile := opts.MaxHitsPerFile
|
|
if maxPerFile <= 0 {
|
|
maxPerFile = fileAIDefaultMaxHitsPerFile
|
|
}
|
|
if maxPerFile > fileAIHardMaxHitsPerFile {
|
|
maxPerFile = fileAIHardMaxHitsPerFile
|
|
}
|
|
maxTotal := opts.MaxTotalHits
|
|
if maxTotal <= 0 {
|
|
maxTotal = fileAIDefaultMaxTotalHits
|
|
}
|
|
if maxTotal > fileAIHardMaxTotalHits {
|
|
maxTotal = fileAIHardMaxTotalHits
|
|
}
|
|
maxTextRunes := opts.MaxHitTextRunes
|
|
if maxTextRunes <= 0 {
|
|
maxTextRunes = fileAIDefaultMaxHitTextRunes
|
|
}
|
|
if maxTextRunes > fileAIHardMaxHitTextRunes {
|
|
maxTextRunes = fileAIHardMaxHitTextRunes
|
|
}
|
|
|
|
hits = make([]FileAIContentHit, 0, 32)
|
|
totalHits := 0
|
|
|
|
for _, it := range inventory {
|
|
rel := strings.TrimSpace(it.RelPath)
|
|
if rel == "" {
|
|
continue
|
|
}
|
|
if !opts.passesInventoryMeta(it, rel) {
|
|
continue
|
|
}
|
|
if it.Size > maxFile {
|
|
continue
|
|
}
|
|
if it.Size < 0 {
|
|
continue
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(rel))
|
|
if _, bad := fileAIBinaryExt[ext]; bad {
|
|
continue
|
|
}
|
|
|
|
fullPath := filepath.Join(root, filepath.FromSlash(rel))
|
|
fullPath = filepath.Clean(fullPath)
|
|
underRoot, err := filepath.Rel(root, fullPath)
|
|
if err != nil || strings.HasPrefix(underRoot, "..") {
|
|
continue
|
|
}
|
|
if ShouldFilterSensitivePath(fullPath) {
|
|
continue
|
|
}
|
|
|
|
if scannedFiles >= maxScan {
|
|
scanTruncated = true
|
|
break
|
|
}
|
|
|
|
st, err := os.Stat(fullPath)
|
|
if err != nil || st.IsDir() {
|
|
continue
|
|
}
|
|
if st.Size() > maxFile {
|
|
continue
|
|
}
|
|
|
|
fileHits, err := scanFileForMatch(fullPath, match, maxPerFile, maxTextRunes)
|
|
scannedFiles++
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, h := range fileHits {
|
|
if totalHits >= maxTotal {
|
|
scanTruncated = true
|
|
return hits, scannedFiles, scanTruncated
|
|
}
|
|
hits = append(hits, h)
|
|
totalHits++
|
|
}
|
|
}
|
|
|
|
return hits, scannedFiles, scanTruncated
|
|
}
|
|
|
|
func scanFileForMatch(absPath string, match func(string) bool, maxPerFile, maxTextRunes int) ([]FileAIContentHit, error) {
|
|
f, err := os.Open(absPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
buf := make([]byte, 0, 64*1024)
|
|
scanner.Buffer(buf, 1024*1024)
|
|
|
|
var out []FileAIContentHit
|
|
lineNo := 0
|
|
for scanner.Scan() {
|
|
lineNo++
|
|
if len(out) >= maxPerFile {
|
|
break
|
|
}
|
|
line := scanner.Text()
|
|
if !match(line) {
|
|
continue
|
|
}
|
|
out = append(out, FileAIContentHit{
|
|
Path: absPath,
|
|
Line: lineNo,
|
|
Text: truncateHitLine(line, maxTextRunes),
|
|
})
|
|
}
|
|
return out, scanner.Err()
|
|
}
|
|
|
|
func truncateHitLine(s string, maxRunes int) string {
|
|
if maxRunes <= 0 {
|
|
maxRunes = fileAIDefaultMaxHitTextRunes
|
|
}
|
|
if utf8.RuneCountInString(s) <= maxRunes {
|
|
return strings.TrimRight(s, "\r\n")
|
|
}
|
|
var b strings.Builder
|
|
n := 0
|
|
for _, r := range s {
|
|
if n >= maxRunes {
|
|
b.WriteString("…")
|
|
break
|
|
}
|
|
b.WriteRune(r)
|
|
n++
|
|
}
|
|
return strings.TrimRight(b.String(), "\r\n")
|
|
}
|
|
|
|
func FormatContentHitsForPrompt(hits []FileAIContentHit, maxBytes int, matchDesc string) string {
|
|
if len(hits) == 0 || maxBytes <= 0 {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
if matchDesc == "" {
|
|
matchDesc = "search"
|
|
}
|
|
b.WriteString("Content line matches (" + matchDesc + "). Do not invent lines; only reference these.\n")
|
|
for _, h := range hits {
|
|
var line string
|
|
if h.Line <= 0 {
|
|
line = h.Path + " (filename): " + h.Text + "\n"
|
|
} else {
|
|
line = h.Path + ":" + strconv.Itoa(h.Line) + ": " + h.Text + "\n"
|
|
}
|
|
if b.Len()+len(line) > maxBytes {
|
|
b.WriteString("... (further hits omitted for prompt size)\n")
|
|
break
|
|
}
|
|
b.WriteString(line)
|
|
}
|
|
return b.String()
|
|
}
|