mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-07 00:07:30 +08:00
fix bug
This commit is contained in:
282
pkg/tools/repo_map.go
Normal file
282
pkg/tools/repo_map.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RepoMapTool struct {
|
||||
workspace string
|
||||
}
|
||||
|
||||
func NewRepoMapTool(workspace string) *RepoMapTool {
|
||||
return &RepoMapTool{workspace: workspace}
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) Name() string {
|
||||
return "repo_map"
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) Description() string {
|
||||
return "Build and query repository map to quickly locate target files/symbols before reading source."
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Search file path or symbol keyword",
|
||||
},
|
||||
"max_results": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"default": 20,
|
||||
"description": "Maximum results to return",
|
||||
},
|
||||
"refresh": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "Force rebuild map cache",
|
||||
"default": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type repoMapCache struct {
|
||||
Workspace string `json:"workspace"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Files []repoMapEntry `json:"files"`
|
||||
}
|
||||
|
||||
type repoMapEntry struct {
|
||||
Path string `json:"path"`
|
||||
Lang string `json:"lang"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime int64 `json:"mod_time"`
|
||||
Symbols []string `json:"symbols,omitempty"`
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) Execute(_ context.Context, args map[string]interface{}) (string, error) {
|
||||
query, _ := args["query"].(string)
|
||||
maxResults := 20
|
||||
if raw, ok := args["max_results"].(float64); ok && raw > 0 {
|
||||
maxResults = int(raw)
|
||||
}
|
||||
forceRefresh, _ := args["refresh"].(bool)
|
||||
|
||||
cache, err := t.loadOrBuildMap(forceRefresh)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(cache.Files) == 0 {
|
||||
return "Repo map is empty.", nil
|
||||
}
|
||||
|
||||
results := t.filterRepoMap(cache.Files, query)
|
||||
if len(results) > maxResults {
|
||||
results = results[:maxResults]
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Repo Map (updated: %s)\n", time.UnixMilli(cache.UpdatedAt).Format("2006-01-02 15:04:05")))
|
||||
sb.WriteString(fmt.Sprintf("Workspace: %s\n", cache.Workspace))
|
||||
sb.WriteString(fmt.Sprintf("Matched files: %d\n\n", len(results)))
|
||||
|
||||
for _, item := range results {
|
||||
sb.WriteString(fmt.Sprintf("- %s [%s] (%d bytes)\n", item.Path, item.Lang, item.Size))
|
||||
if len(item.Symbols) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" symbols: %s\n", strings.Join(item.Symbols, ", ")))
|
||||
}
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return "No files matched query.", nil
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) cachePath() string {
|
||||
return filepath.Join(t.workspace, ".clawgo", "repo_map.json")
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) loadOrBuildMap(force bool) (*repoMapCache, error) {
|
||||
if !force {
|
||||
if data, err := os.ReadFile(t.cachePath()); err == nil {
|
||||
var cache repoMapCache
|
||||
if err := json.Unmarshal(data, &cache); err == nil {
|
||||
if cache.Workspace == t.workspace && (time.Now().UnixMilli()-cache.UpdatedAt) < int64((10*time.Minute)/time.Millisecond) {
|
||||
return &cache, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache := &repoMapCache{
|
||||
Workspace: t.workspace,
|
||||
UpdatedAt: time.Now().UnixMilli(),
|
||||
Files: []repoMapEntry{},
|
||||
}
|
||||
|
||||
err := filepath.Walk(t.workspace, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info == nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
name := info.Name()
|
||||
if name == ".git" || name == "node_modules" || name == ".clawgo" || name == "vendor" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if info.Size() > 512*1024 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(t.workspace, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
lang := langFromPath(rel)
|
||||
if lang == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry := repoMapEntry{
|
||||
Path: rel,
|
||||
Lang: lang,
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime().UnixMilli(),
|
||||
Symbols: extractSymbols(path, lang),
|
||||
}
|
||||
cache.Files = append(cache.Files, entry)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(cache.Files, func(i, j int) bool {
|
||||
return cache.Files[i].Path < cache.Files[j].Path
|
||||
})
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(t.cachePath()), 0755); err == nil {
|
||||
if data, err := json.Marshal(cache); err == nil {
|
||||
_ = os.WriteFile(t.cachePath(), data, 0644)
|
||||
}
|
||||
}
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
func (t *RepoMapTool) filterRepoMap(files []repoMapEntry, query string) []repoMapEntry {
|
||||
q := strings.ToLower(strings.TrimSpace(query))
|
||||
if q == "" {
|
||||
return files
|
||||
}
|
||||
|
||||
type scored struct {
|
||||
item repoMapEntry
|
||||
score int
|
||||
}
|
||||
items := []scored{}
|
||||
for _, file := range files {
|
||||
score := 0
|
||||
p := strings.ToLower(file.Path)
|
||||
if strings.Contains(p, q) {
|
||||
score += 5
|
||||
}
|
||||
for _, sym := range file.Symbols {
|
||||
if strings.Contains(strings.ToLower(sym), q) {
|
||||
score += 3
|
||||
}
|
||||
}
|
||||
if score > 0 {
|
||||
items = append(items, scored{item: file, score: score})
|
||||
}
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].score == items[j].score {
|
||||
return items[i].item.Path < items[j].item.Path
|
||||
}
|
||||
return items[i].score > items[j].score
|
||||
})
|
||||
|
||||
out := make([]repoMapEntry, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, item.item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func langFromPath(path string) string {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".go":
|
||||
return "go"
|
||||
case ".md":
|
||||
return "markdown"
|
||||
case ".json":
|
||||
return "json"
|
||||
case ".yaml", ".yml":
|
||||
return "yaml"
|
||||
case ".sh":
|
||||
return "shell"
|
||||
case ".py":
|
||||
return "python"
|
||||
case ".js":
|
||||
return "javascript"
|
||||
case ".ts":
|
||||
return "typescript"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func extractSymbols(path, lang string) []string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
content := string(data)
|
||||
out := []string{}
|
||||
|
||||
switch lang {
|
||||
case "go":
|
||||
re := regexp.MustCompile(`(?m)^func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
|
||||
for _, m := range re.FindAllStringSubmatch(content, 12) {
|
||||
if len(m) > 1 {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
}
|
||||
typeRe := regexp.MustCompile(`(?m)^type\s+([A-Za-z_][A-Za-z0-9_]*)\s+`)
|
||||
for _, m := range typeRe.FindAllStringSubmatch(content, 12) {
|
||||
if len(m) > 1 {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
}
|
||||
case "python":
|
||||
re := regexp.MustCompile(`(?m)^def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
|
||||
for _, m := range re.FindAllStringSubmatch(content, 12) {
|
||||
if len(m) > 1 {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(out)
|
||||
uniq := out[:1]
|
||||
for i := 1; i < len(out); i++ {
|
||||
if out[i] != out[i-1] {
|
||||
uniq = append(uniq, out[i])
|
||||
}
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
Reference in New Issue
Block a user