Skip to content

Commit 8129260

Browse files
authored
feat: File management supports AI search (#12415)
1 parent a40786a commit 8129260

File tree

37 files changed

+2319
-17
lines changed

37 files changed

+2319
-17
lines changed

agent/app/api/v2/file.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ func (b *BaseApi) ListFiles(c *gin.Context) {
4848
helper.SuccessWithData(c, fileList)
4949
}
5050

51+
// @Tags File
52+
// @Summary File search: content grep + optional AI summary
53+
// @Description When file-management AI is enabled, returns mode=ai with summary and hits. When disabled, returns mode=grep with hits only. Scans file contents only. Supports match options, extension/size/time filters, and scan limits.
54+
// @Accept json
55+
// @Param request body request.FileAISearch true "request"
56+
// @Success 200 {object} response.FileAISearchResult
57+
// @Security ApiKeyAuth
58+
// @Security Timestamp
59+
// @Router /files/ai-search [post]
60+
func (b *BaseApi) FileAISearch(c *gin.Context) {
61+
var req request.FileAISearch
62+
if err := helper.CheckBindAndValidate(&req, c); err != nil {
63+
return
64+
}
65+
res, err := fileService.AISearch(req)
66+
if err != nil {
67+
helper.InternalServer(c, err)
68+
return
69+
}
70+
helper.SuccessWithData(c, res)
71+
}
72+
5173
// @Tags File
5274
// @Summary Page file
5375
// @Accept json

agent/app/api/v2/setting.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,27 @@ func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
8181
helper.Success(c)
8282
}
8383

84+
func (b *BaseApi) GetFileManageAISettingInfo(c *gin.Context) {
85+
setting, err := settingService.GetFileManageAIInfo()
86+
if err != nil {
87+
helper.InternalServer(c, err)
88+
return
89+
}
90+
helper.SuccessWithData(c, setting)
91+
}
92+
93+
func (b *BaseApi) UpdateFileManageAISetting(c *gin.Context) {
94+
var req dto.FileManageAIInfo
95+
if err := helper.CheckBindAndValidate(&req, c); err != nil {
96+
return
97+
}
98+
if err := settingService.UpdateFileManageAI(req); err != nil {
99+
helper.InternalServer(c, err)
100+
return
101+
}
102+
helper.Success(c)
103+
}
104+
84105
// @Tags System Setting
85106
// @Summary Load local backup dir
86107
// @Success 200 {string} path

agent/app/dto/request/file.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@ type FileOption struct {
99
files.FileOption
1010
}
1111

12+
type FileAISearch struct {
13+
Path string `json:"path" validate:"required"`
14+
Query string `json:"query" validate:"required"`
15+
ContainSub *bool `json:"containSub,omitempty"`
16+
MaxItems int `json:"maxItems" validate:"omitempty,min=1,max=2000"`
17+
MatchCase bool `json:"matchCase"`
18+
WholeWord bool `json:"wholeWord"`
19+
UseRegex bool `json:"useRegex"`
20+
Extensions []string `json:"extensions,omitempty"`
21+
MinSize int64 `json:"minSize"`
22+
MaxSize int64 `json:"maxSize"`
23+
ModifiedAfter string `json:"modifiedAfter,omitempty"`
24+
ModifiedBefore string `json:"modifiedBefore,omitempty"`
25+
26+
MaxScanFiles int `json:"maxScanFiles"`
27+
MaxFileBytes int64 `json:"maxFileBytes"`
28+
MaxHitsPerFile int `json:"maxHitsPerFile"`
29+
MaxTotalHits int `json:"maxTotalHits"`
30+
ContentHitsPromptMaxBytes int `json:"contentHitsPromptMaxBytes"`
31+
LlmMaxOutputTokens int `json:"llmMaxOutputTokens"`
32+
}
33+
1234
type FileContentReq struct {
1335
Path string `json:"path" validate:"required"`
1436
IsDetail bool `json:"isDetail"`

agent/app/dto/response/file.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,24 @@ type FileConvertLog struct {
8585
type FileRemarksRes struct {
8686
Remarks map[string]string `json:"remarks"`
8787
}
88+
89+
type FileAIContentHit struct {
90+
Path string `json:"path"`
91+
Line int `json:"line"`
92+
Text string `json:"text"`
93+
}
94+
95+
type FileAISearchResult struct {
96+
Mode string `json:"mode"`
97+
Summary string `json:"summary"`
98+
Hits []FileAIContentHit `json:"hits"`
99+
ContentScannedFiles int `json:"contentScannedFiles"`
100+
ContentHitsTruncated bool `json:"contentHitsTruncated"`
101+
Truncated bool `json:"truncated"`
102+
PreFiltered bool `json:"preFiltered"`
103+
ItemCount int `json:"itemCount"`
104+
PromptTokens int `json:"promptTokens"`
105+
CompletionTokens int `json:"completionTokens"`
106+
TotalTokens int `json:"totalTokens"`
107+
Duration string `json:"duration"`
108+
}

agent/app/dto/setting.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ type TerminalAIInfo struct {
9696
AIRiskCommandsDefault string `json:"aiRiskCommandsDefault"`
9797
}
9898

99+
type FileManageAIInfo struct {
100+
AIStatus string `json:"aiStatus"`
101+
AIAccountID string `json:"aiAccountId"`
102+
}
103+
99104
type CommonDescription struct {
100105
ID string `json:"id" validate:"required"`
101106
Type string `json:"type" validate:"required"`

agent/app/service/agents.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
494494
return err
495495
}
496496
terminalai.InvalidateTerminalRuntimeCache()
497+
terminalai.InvalidateFileAIRuntimeCache()
497498
if req.SyncAgents {
498499
if err := a.syncAgentsByAccount(account); err != nil {
499500
return err
@@ -634,6 +635,7 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
634635
return err
635636
}
636637
terminalai.InvalidateTerminalRuntimeCache()
638+
terminalai.InvalidateFileAIRuntimeCache()
637639
return a.syncAgentsByAccount(account)
638640
}
639641

@@ -666,6 +668,7 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
666668
return err
667669
}
668670
terminalai.InvalidateTerminalRuntimeCache()
671+
terminalai.InvalidateFileAIRuntimeCache()
669672
return a.syncAgentsByAccount(account)
670673
}
671674

@@ -694,6 +697,7 @@ func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
694697
return err
695698
}
696699
terminalai.InvalidateTerminalRuntimeCache()
700+
terminalai.InvalidateFileAIRuntimeCache()
697701
return agentAccountRepo.DeleteByID(req.ID)
698702
}
699703

agent/app/service/file.go

Lines changed: 155 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/1Panel-dev/1Panel/agent/global"
4242
"github.com/1Panel-dev/1Panel/agent/utils/common"
4343
"github.com/1Panel-dev/1Panel/agent/utils/files"
44+
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
4445
"github.com/pkg/errors"
4546
)
4647

@@ -78,10 +79,7 @@ type IFileService interface {
7879
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
7980
BatchGetRemarks(req request.FileRemarkBatch) map[string]string
8081
SetRemark(req request.FileRemarkUpdate) error
81-
}
82-
83-
var filteredPaths = []string{
84-
"/.1panel_clash",
82+
AISearch(req request.FileAISearch) (*response.FileAISearchResult, error)
8583
}
8684

8785
const (
@@ -170,14 +168,7 @@ func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, e
170168
}
171169

172170
func shouldFilterPath(path string) bool {
173-
cleanedPath := filepath.Clean(path)
174-
for _, filteredPath := range filteredPaths {
175-
cleanedFilteredPath := filepath.Clean(filteredPath)
176-
if cleanedFilteredPath == cleanedPath || strings.HasPrefix(cleanedPath, cleanedFilteredPath+"/") {
177-
return true
178-
}
179-
}
180-
return false
171+
return files.ShouldFilterSensitivePath(path)
181172
}
182173

183174
func (f *FileService) buildFileTree(node *response.FileTree, items []*files.FileInfo, op request.FileOption, level int) error {
@@ -1019,3 +1010,155 @@ func (f *FileService) ConvertLog(req dto.PageInfo) (total int64, data []response
10191010

10201011
return total, data, nil
10211012
}
1013+
1014+
func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearchResult, error) {
1015+
root := filepath.Clean(strings.TrimSpace(req.Path))
1016+
if root == "" {
1017+
return nil, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
1018+
}
1019+
query := strings.TrimSpace(req.Query)
1020+
if query == "" {
1021+
return nil, buserr.WithDetail("ErrInvalidParams", "query is required", nil)
1022+
}
1023+
st, err := os.Stat(root)
1024+
if err != nil {
1025+
if os.IsNotExist(err) {
1026+
return nil, buserr.New("ErrPathNotFound")
1027+
}
1028+
return nil, err
1029+
}
1030+
if !st.IsDir() {
1031+
return nil, buserr.New("ErrPathNotFound")
1032+
}
1033+
1034+
maxItems := req.MaxItems
1035+
if maxItems <= 0 {
1036+
maxItems = files.DefaultFileAIMaxItems
1037+
}
1038+
if maxItems > 2000 {
1039+
maxItems = 2000
1040+
}
1041+
1042+
containSub := true
1043+
if req.ContainSub != nil {
1044+
containSub = *req.ContainSub
1045+
}
1046+
1047+
searchOpts, err := files.MergeContentSearchOptions(
1048+
req.MatchCase, req.WholeWord, req.UseRegex,
1049+
req.Extensions,
1050+
req.MinSize, req.MaxSize,
1051+
req.ModifiedAfter, req.ModifiedBefore,
1052+
req.MaxScanFiles,
1053+
req.MaxFileBytes,
1054+
req.MaxHitsPerFile, req.MaxTotalHits,
1055+
req.ContentHitsPromptMaxBytes,
1056+
req.LlmMaxOutputTokens,
1057+
)
1058+
if err != nil {
1059+
return nil, buserr.WithDetail("ErrInvalidParams", err.Error(), nil)
1060+
}
1061+
1062+
matchFn, err := files.NewContentLineMatcher(query, searchOpts)
1063+
if err != nil {
1064+
return nil, buserr.WithDetail("ErrFileAISearchBadPattern", err.Error(), nil)
1065+
}
1066+
1067+
cfg, timeout, err := terminalai.LoadFileAIRuntimeConfig()
1068+
aiEnabled := err == nil
1069+
if err != nil && !errors.Is(err, os.ErrNotExist) {
1070+
return nil, err
1071+
}
1072+
1073+
items, truncated, err := files.CollectDirInventory(root, containSub, maxItems)
1074+
if err != nil {
1075+
return nil, err
1076+
}
1077+
1078+
preFiltered := false
1079+
llmItems := items
1080+
qLower := strings.ToLower(query)
1081+
if len(llmItems) > 0 && query != "" {
1082+
filtered := make([]files.AISearchInventoryItem, 0, len(llmItems))
1083+
for _, it := range llmItems {
1084+
rel := strings.TrimSpace(it.RelPath)
1085+
if rel == "" {
1086+
continue
1087+
}
1088+
if !req.UseRegex && !req.MatchCase && !req.WholeWord && strings.Contains(strings.ToLower(rel), qLower) {
1089+
filtered = append(filtered, it)
1090+
}
1091+
}
1092+
if len(filtered) >= 8 {
1093+
llmItems = filtered
1094+
preFiltered = true
1095+
}
1096+
}
1097+
1098+
start := time.Now()
1099+
contentHits, scannedFiles, hitsTrunc := files.SearchFileAIContentHits(root, llmItems, searchOpts, matchFn)
1100+
hitsDTO := make([]response.FileAIContentHit, 0, len(contentHits))
1101+
for _, h := range contentHits {
1102+
hitsDTO = append(hitsDTO, response.FileAIContentHit{Path: h.Path, Line: h.Line, Text: h.Text})
1103+
}
1104+
1105+
matchDesc := searchOpts.ContentMatchDescription()
1106+
result := &response.FileAISearchResult{
1107+
Hits: hitsDTO,
1108+
ContentScannedFiles: scannedFiles,
1109+
ContentHitsTruncated: hitsTrunc,
1110+
Truncated: truncated,
1111+
PreFiltered: preFiltered,
1112+
ItemCount: len(llmItems),
1113+
}
1114+
1115+
if len(llmItems) == 0 {
1116+
if aiEnabled {
1117+
result.Mode = "ai"
1118+
result.Summary = i18n.GetMsgByKey("FileAISearchEmptyDir")
1119+
if result.Summary == "" || result.Summary == "FileAISearchEmptyDir" {
1120+
result.Summary = "No files or directories found under this path (or all entries were filtered)."
1121+
}
1122+
} else {
1123+
result.Mode = "grep"
1124+
result.Summary = ""
1125+
}
1126+
result.Duration = time.Since(start).Round(time.Millisecond).String()
1127+
return result, nil
1128+
}
1129+
1130+
if !aiEnabled {
1131+
result.Mode = "grep"
1132+
result.Summary = ""
1133+
result.Duration = time.Since(start).Round(time.Millisecond).String()
1134+
return result, nil
1135+
}
1136+
1137+
result.Mode = "ai"
1138+
1139+
clientTimeout := timeout
1140+
if clientTimeout < 30*time.Second {
1141+
clientTimeout = 90 * time.Second
1142+
}
1143+
if clientTimeout > 5*time.Minute {
1144+
clientTimeout = 5 * time.Minute
1145+
}
1146+
1147+
runCtx, cancel := context.WithTimeout(context.Background(), timeout+time.Minute)
1148+
defer cancel()
1149+
1150+
llmMaxOut := searchOpts.LlmMaxOutputTokens
1151+
summary, usage, err := files.RunFileAISearchLLM(runCtx, cfg, clientTimeout, root, query, llmItems, truncated, preFiltered, contentHits, scannedFiles, hitsTrunc, matchDesc, searchOpts.ContentHitsPromptMaxBytes, llmMaxOut)
1152+
if err != nil {
1153+
return nil, err
1154+
}
1155+
result.Summary = summary
1156+
result.PromptTokens = usage.PromptTokens
1157+
result.CompletionTokens = usage.CompletionTokens
1158+
result.TotalTokens = usage.TotalTokens
1159+
if result.TotalTokens == 0 {
1160+
result.TotalTokens = usage.PromptTokens + usage.CompletionTokens
1161+
}
1162+
result.Duration = time.Since(start).Round(time.Millisecond).String()
1163+
return result, nil
1164+
}

0 commit comments

Comments
 (0)