/ common / agent / tasks.go
tasks.go
  1  // Copyright (c) 2024-2026 Tencent Zhuque Lab. All rights reserved.
  2  //
  3  // Licensed under the Apache License, Version 2.0 (the "License");
  4  // you may not use this file except in compliance with the License.
  5  // You may obtain a copy of the License at
  6  //
  7  //     http://www.apache.org/licenses/LICENSE-2.0
  8  //
  9  // Unless required by applicable law or agreed to in writing, software
 10  // distributed under the License is distributed on an "AS IS" BASIS,
 11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  // See the License for the specific language governing permissions and
 13  // limitations under the License.
 14  //
 15  // Requirement: Any integration or derivative work must explicitly attribute
 16  // Tencent Zhuque Lab (https://github.com/Tencent/AI-Infra-Guard) in its
 17  // documentation or user interface, as detailed in the NOTICE file.
 18  
 19  package agent
 20  
 21  import (
 22  	"context"
 23  	"encoding/json"
 24  	"fmt"
 25  	"os"
 26  	"path"
 27  	"path/filepath"
 28  	"strings"
 29  	"sync"
 30  	"time"
 31  
 32  	"github.com/Tencent/AI-Infra-Guard/pkg/vulstruct"
 33  	iputil "github.com/projectdiscovery/utils/ip"
 34  
 35  	"github.com/Tencent/AI-Infra-Guard/common/utils"
 36  
 37  	"github.com/Tencent/AI-Infra-Guard/common/utils/models"
 38  
 39  	"github.com/Tencent/AI-Infra-Guard/common/runner"
 40  	"github.com/Tencent/AI-Infra-Guard/internal/gologger"
 41  	"github.com/Tencent/AI-Infra-Guard/internal/options"
 42  	"github.com/google/uuid"
 43  )
 44  
 45  // ResultCallback 任务结果回调函数类型
 46  type ResultCallback func(result map[string]interface{})
 47  
 48  // ActionLogCallback 插件日志回调函数类型
 49  type ActionLogCallback func(actionId, tool, planStepId, actionLog string)
 50  
 51  // ToolUsedCallback 插件工作状态回调函数类型
 52  type ToolUsedCallback func(planStepId, statusId, description string, tools []Tool)
 53  
 54  // NewPlanStepCallback 新建执行步骤回调函数类型
 55  type NewPlanStepCallback func(stepId, title string)
 56  
 57  // StatusUpdateCallback 更新步骤状态回调函数类型
 58  type StatusUpdateCallback func(planStepId, statusId, agentStatus, brief, description string)
 59  
 60  // PlanUpdateCallback 更新任务计划回调函数类型
 61  type PlanUpdateCallback func(tasks []SubTask)
 62  
 63  type ErrorCallback func(error string)
 64  
 65  // TaskCallbacks 任务回调函数集合
 66  type TaskCallbacks struct {
 67  	ResultCallback           ResultCallback       // 任务结果回调
 68  	ToolUseLogCallback       ActionLogCallback    // 插件日志回调
 69  	ToolUsedCallback         ToolUsedCallback     // 插件状态回调
 70  	NewPlanStepCallback      NewPlanStepCallback  // 新建执行步骤回调
 71  	StepStatusUpdateCallback StatusUpdateCallback // 更新步骤状态回调
 72  	PlanUpdateCallback       PlanUpdateCallback   // 更新任务计划回调
 73  	ErrorCallback            ErrorCallback        // 错误回调
 74  }
 75  
 76  type TaskInterface interface {
 77  	GetName() string
 78  	Execute(ctx context.Context, request TaskRequest, callbacks TaskCallbacks) error
 79  }
 80  
 81  // ScanRequest 扫描请求结构
 82  type ScanRequest struct {
 83  	Target  []string          `json:"-"`
 84  	Headers map[string]string `json:"headers"`
 85  	Timeout int               `json:"timeout,omitempty"`
 86  	Model   struct {
 87  		Model              string `json:"model"`
 88  		Token              string `json:"token"`
 89  		BaseUrl            string `json:"base_url"`
 90  	} `json:"model,omitempty"`
 91  }
 92  
 93  type AIInfraScanAgent struct {
 94  	Server string
 95  }
 96  
 97  func (t *AIInfraScanAgent) GetName() string {
 98  	return TaskTypeAIInfraScan
 99  }
100  func (t *AIInfraScanAgent) Execute(ctx context.Context, request TaskRequest, callbacks TaskCallbacks) error {
101  	var reqScan ScanRequest
102  	if len(request.Params) > 0 {
103  		if err := json.Unmarshal(request.Params, &reqScan); err != nil {
104  			return err
105  		}
106  	}
107  
108  	language := request.Language
109  	if language == "" {
110  		language = "zh"
111  	}
112  
113  	// 初始化语言文本
114  	texts := initTexts(language)
115  
116  	// 处理目标和附件
117  	targets, err := t.prepareTargets(request, reqScan, texts)
118  	if err != nil {
119  		return err
120  	}
121  	reqScan.Target = targets
122  
123  	if reqScan.Timeout == 0 {
124  		reqScan.Timeout = 30
125  	}
126  
127  	// 判断是否使用AI模式
128  	useAI := reqScan.Model.Token != "" || reqScan.Model.BaseUrl != ""
129  	var model *models.OpenAI
130  	if useAI {
131  		if reqScan.Model.BaseUrl == "" && reqScan.Model.Model == "" {
132  			return fmt.Errorf("model parameters are required")
133  		}
134  		model = &models.OpenAI{
135  			BaseUrl:            reqScan.Model.BaseUrl,
136  			Model:              reqScan.Model.Model,
137  			Key:                reqScan.Model.Token,
138  		}
139  	}
140  
141  	return t.executeScan(ctx, request, reqScan, texts, callbacks, model)
142  }
143  
144  // scanTexts 包含所有语言相关的文本
145  type scanTexts struct {
146  	initEnv, execScan, genReport, aigWorking, aigCompleted, initConfig, portDetection, targetConfig, scanComplete, reportGen                               string
147  	createTempDir, downloadFile, readFile, portScan, foundPort, portCount, targetCount, foundVuln, noVuln, execError, config, scanResult                   string
148  	initDescTemplate, portDetectDescTemplate, portCompleteTemplate, targetCountTemplate, webAppTemplate, vulnFoundTemplate, noVulnTemplate, errorTemplate  string
149  	scanningDesc, execScanDesc, scanCompleteDesc, taskCompleteDesc, reportGenDesc, reportGenToolDesc, scanResultTemplate                                   string
150  	downloadFileLog, execScanTool, scanTool, generateReportTool, nmapTool, aiScannerTool, reportGeneratorTool, scanOperation, targetSystem, generateReport string
151  }
152  
153  // initTexts 初始化语言文本
154  func initTexts(language string) scanTexts {
155  	var texts scanTexts
156  	if language == "en" {
157  		texts.initEnv = "Initialize scan environment"
158  		texts.execScan = "Execute AI infrastructure scan"
159  		texts.genReport = "Generate scan report"
160  		texts.aigWorking = "A.I.G is working"
161  		texts.aigCompleted = "A.I.G completed work"
162  		texts.initConfig = "Initialization configuration completed"
163  		texts.portDetection = "Auto-detecting ports"
164  		texts.targetConfig = "Target configuration completed"
165  		texts.scanComplete = "Scan completed"
166  		texts.reportGen = "Generating scan report"
167  		texts.createTempDir = "Failed to create temporary directory"
168  		texts.downloadFile = "Failed to download file"
169  		texts.readFile = "Failed to read file"
170  		texts.portScan = "Port scan"
171  		texts.foundPort = "Found port"
172  		texts.portCount = "Port count"
173  		texts.targetCount = "Target count"
174  		texts.foundVuln = "Found vulnerabilities"
175  		texts.noVuln = "Scan completed, no vulnerabilities found"
176  		texts.execError = "Execution error"
177  		texts.config = "Configuration"
178  		texts.scanResult = "Scan result"
179  		texts.initDescTemplate = "Starting to initialize AI infrastructure scan environment"
180  		texts.portDetectDescTemplate = "Auto-detecting IP: %s"
181  		texts.portCompleteTemplate = "%s port detection completed"
182  		texts.targetCountTemplate = "Target count: %d"
183  		texts.webAppTemplate = "WEB Application: %s "
184  		texts.vulnFoundTemplate = "URL:%s %sFound vulnerabilities:%d\n"
185  		texts.noVulnTemplate = "URL:%s %sScan completed, no vulnerabilities found\n"
186  		texts.errorTemplate = "Execution error: host:%s %s\n"
187  		texts.scanningDesc = "Scanning..."
188  		texts.execScanDesc = "Executing AI infrastructure scan"
189  		texts.scanCompleteDesc = "Scan completed"
190  		texts.taskCompleteDesc = "AI infrastructure scan task completed"
191  		texts.reportGenDesc = "Generating scan report"
192  		texts.reportGenToolDesc = "Generating scan report"
193  		texts.scanResultTemplate = "Scan results: %d items"
194  		texts.downloadFileLog = "Starting to download file: %s"
195  		texts.execScanTool = "Execute scan"
196  		texts.scanTool = "Scan"
197  		texts.generateReportTool = "Generate report"
198  		texts.nmapTool = "nmap"
199  		texts.aiScannerTool = "ai_scanner"
200  		texts.reportGeneratorTool = "report_generator"
201  		texts.scanOperation = "Scan"
202  		texts.targetSystem = "Target system"
203  		texts.generateReport = "Generate report"
204  	} else {
205  		texts.initEnv = "准备扫描环境"
206  		texts.execScan = "执行深度扫描"
207  		texts.genReport = "智能分析与报告生成"
208  		texts.aigWorking = "Agent正在工作"
209  		texts.aigCompleted = "Agent完成工作"
210  		texts.initConfig = "初始化配置完成"
211  		texts.portDetection = "正在自动识别端口"
212  		texts.targetConfig = "目标配置完成"
213  		texts.scanComplete = "扫描完成"
214  		texts.reportGen = "生成扫描报告"
215  		texts.createTempDir = "创建临时目录失败"
216  		texts.downloadFile = "下载文件失败"
217  		texts.readFile = "读取文件失败"
218  		texts.portScan = "端口扫描"
219  		texts.foundPort = "发现端口"
220  		texts.portCount = "端口数量"
221  		texts.targetCount = "目标数量"
222  		texts.foundVuln = "发现漏洞"
223  		texts.noVuln = "扫描完成,未发现漏洞"
224  		texts.execError = "执行错误"
225  		texts.config = "配置"
226  		texts.scanResult = "Agent发现"
227  		texts.initDescTemplate = "开始初始化AI基础设施扫描环境"
228  		texts.portDetectDescTemplate = "正在自动识别IP: %s"
229  		texts.portCompleteTemplate = "%s 端口探测完成"
230  		texts.targetCountTemplate = "目标数量: %d"
231  		texts.webAppTemplate = "WEB应用: %s "
232  		texts.vulnFoundTemplate = "URL:%s %s发现漏洞:%d\n"
233  		texts.noVulnTemplate = "URL:%s %s扫描完成,未发现漏洞\n"
234  		texts.errorTemplate = "执行错误: host:%s %s\n"
235  		texts.scanningDesc = "开始对目标系统进行多维度扫描"
236  		texts.execScanDesc = "正在执行AI基础设施扫描"
237  		texts.scanCompleteDesc = "完成扫描"
238  		texts.taskCompleteDesc = "AI基础设施扫描任务完成"
239  		texts.reportGenDesc = "我需要提供更有价值的洞察..."
240  		texts.reportGenToolDesc = "正在生成扫描报告"
241  		texts.scanResultTemplate = "扫描结果: %d 条"
242  		texts.downloadFileLog = "开始下载文件: %s"
243  		texts.execScanTool = "执行扫描"
244  		texts.scanTool = "扫描"
245  		texts.generateReportTool = "生成报告"
246  		texts.nmapTool = "nmap"
247  		texts.aiScannerTool = "ai_scanner"
248  		texts.reportGeneratorTool = "report_generator"
249  		texts.scanOperation = "扫描"
250  		texts.targetSystem = "目标系统"
251  		texts.generateReport = "生成结构化扫描报告"
252  	}
253  	return texts
254  }
255  
256  // prepareTargets 处理目标和附件
257  func (t *AIInfraScanAgent) prepareTargets(request TaskRequest, reqScan ScanRequest, texts scanTexts) ([]string, error) {
258  	targets := strings.Split(strings.TrimSpace(request.Content), "\n")
259  
260  	if len(request.Attachments) == 0 {
261  		return targets, nil
262  	}
263  
264  	tempDir := "temp_uploads"
265  	if err := os.MkdirAll(tempDir, 0755); err != nil {
266  		gologger.Errorf("%s: %v", texts.createTempDir, err)
267  		return nil, err
268  	}
269  
270  	for _, file := range request.Attachments {
271  		gologger.Infof(texts.downloadFileLog, file)
272  		fileName := filepath.Join(tempDir, fmt.Sprintf("tmp-%d%s", time.Now().UnixMicro(), filepath.Ext(file)))
273  		// Verify the path is within tempDir to prevent path traversal
274  		absTempDir, _ := filepath.Abs(tempDir)
275  		absFileName, _ := filepath.Abs(fileName)
276  		if !strings.HasPrefix(absFileName, absTempDir+string(os.PathSeparator)) {
277  			gologger.WithError(fmt.Errorf("非法路径: %s", file)).Errorln(texts.downloadFile)
278  			return nil, fmt.Errorf("非法文件路径")
279  		}
280  		if err := utils.DownloadFile(t.Server, request.SessionId, file, fileName); err != nil {
281  			gologger.WithError(err).Errorln(texts.downloadFile)
282  			return nil, err
283  		}
284  		lines, err := os.ReadFile(fileName)
285  		if err != nil {
286  			gologger.WithError(err).Errorln(texts.readFile)
287  			return nil, err
288  		}
289  		targets = append(targets, strings.Split(string(lines), "\n")...)
290  	}
291  
292  	return targets, nil
293  }
294  
295  // scanPortsAndPrepareTargets 扫描端口并准备最终目标列表
296  func (t *AIInfraScanAgent) scanPortsAndPrepareTargets(targets []string, step01 string, texts scanTexts, callbacks TaskCallbacks) ([]string, error) {
297  	finalTargets := []string{}
298  	var hosts []string
299  
300  	for _, target := range targets {
301  		if iputil.IsIP(target) {
302  			hosts = append(hosts, target)
303  		}
304  		finalTargets = append(finalTargets, target)
305  	}
306  
307  	for _, host := range hosts {
308  		statusNmap := uuid.NewString()
309  		toolId := uuid.NewString()
310  		callbacks.StepStatusUpdateCallback(step01, statusNmap, AgentStatusRunning, texts.portDetection, fmt.Sprintf(texts.portDetectDescTemplate, host))
311  		callbacks.ToolUsedCallback(step01, statusNmap, texts.nmapTool, []Tool{
312  			CreateTool(toolId, texts.nmapTool, SubTaskStatusDoing, texts.portScan, texts.nmapTool, "-T4 -p 11434,1337,7000-9000,18789", ""),
313  		})
314  
315  		portScanResult, err := utils.NmapScan(host, "11434,1337,7000-9000,18789")
316  		if err != nil {
317  			return nil, err
318  		}
319  
320  		success := 0
321  		for _, port := range portScanResult.Hosts {
322  			address := port.Address.Addr
323  			for _, ported := range port.Ports.PortList {
324  				if ported.State.State == "open" {
325  					finalTargets = append(finalTargets, fmt.Sprintf("%s:%d", address, ported.PortID))
326  					success += 1
327  					callbacks.ToolUseLogCallback(toolId, texts.nmapTool, step01, fmt.Sprintf("%s: %s:%d\n", texts.foundPort, address, ported.PortID))
328  				}
329  			}
330  		}
331  
332  		callbacks.ToolUsedCallback(step01, statusNmap, texts.nmapTool, []Tool{
333  			CreateTool(toolId, texts.nmapTool, SubTaskStatusDone, texts.portScan, texts.nmapTool, "-T4", fmt.Sprintf("%s: %d", texts.portCount, success)),
334  		})
335  		callbacks.StepStatusUpdateCallback(step01, statusNmap, AgentStatusCompleted, fmt.Sprintf(texts.portCompleteTemplate, host), "")
336  	}
337  
338  	return finalTargets, nil
339  }
340  
341  // executeScan 执行扫描任务的统一入口
342  func (t *AIInfraScanAgent) executeScan(ctx context.Context, request TaskRequest, reqScan ScanRequest, texts scanTexts, callbacks TaskCallbacks, model *models.OpenAI) error {
343  	// 创建任务计划
344  	taskTitles := []string{texts.initEnv, texts.execScan, texts.genReport}
345  	var tasks []SubTask
346  	for _, title := range taskTitles {
347  		tasks = append(tasks, CreateSubTask(SubTaskStatusTodo, title, 0, uuid.NewString()))
348  	}
349  	callbacks.PlanUpdateCallback(tasks)
350  
351  	// 步骤1: 初始化环境
352  	step01 := tasks[0].StepId
353  	callbacks.NewPlanStepCallback(step01, texts.initEnv)
354  	statusId01 := uuid.NewString()
355  	callbacks.StepStatusUpdateCallback(step01, statusId01, AgentStatusRunning, "Thinking", "")
356  
357  	// 配置选项
358  	opts := &options.Options{
359  		TimeOut:      reqScan.Timeout,
360  		RateLimit:    200,
361  		FPTemplates:  t.Server,
362  		AdvTemplates: t.Server,
363  		WebServer:    false,
364  		Target:       reqScan.Target,
365  		LoadRemote:   true,
366  	}
367  
368  	headers := make([]string, 0)
369  	for k, v := range reqScan.Headers {
370  		headers = append(headers, k+":"+v)
371  	}
372  	opts.Headers = headers
373  
374  	// 扫描端口并准备目标
375  	targets, err := t.scanPortsAndPrepareTargets(reqScan.Target, step01, texts, callbacks)
376  	if err != nil {
377  		return err
378  	}
379  	opts.Target = targets
380  
381  	// AI模式下的初始化反馈
382  	if model != nil {
383  		config := ""
384  		var configMu sync.Mutex
385  
386  		// 临时回调收集配置信息
387  		tempCallback := func(data interface{}) {
388  			if v, ok := data.(runner.Step01); ok {
389  				configMu.Lock()
390  				config += v.Text + "\n"
391  				configMu.Unlock()
392  			}
393  		}
394  		opts.SetCallback(tempCallback)
395  
396  		// 创建runner获取配置
397  		r, err := runner.New(opts)
398  		if err != nil {
399  			return fmt.Errorf("new runner failed: %v", err)
400  		}
401  		r.Close()
402  
403  		// AI分析初始化配置
404  		prompt := fmt.Sprintf(`你在执行AI基础设施扫描任务,你正在完成todo:%s
405  然后将以下文本转换为进度任务中的todo,加入你自己的思考,而不是简单罗列:
406  target count:%s
407  %s
408  ## 返回格式(只返回json格式)
409  `+"```json\n"+`
410  [
411  {"title":"思考","desc":"在开始扫描前,需要确保所有必要的工具和数据库都已就绪。这就像医生手术前检查器械一样重要。"},
412  {"title":"执行1","desc":"✓ 目标锁定成功 - 识别到1个待扫描目标 ✓ 指纹库加载完成 - 已装载36种识别模式 ✓ 漏洞数据库就绪 - 涵盖394个已知漏洞特征"},
413  {"title":"Agent反思","desc":"环境配置符合预期,所有组件状态良好。我现在已经具备了执行任务所需的全部能力。"},
414  ]
415  `+"\n```\n", texts.initEnv, fmt.Sprintf(texts.targetCountTemplate, len(targets)), config)
416  
417  		if request.Language == "en" {
418  			prompt += "## Return in English"
419  		}
420  
421  		response, err := model.ChatResponse(ctx, prompt)
422  		gologger.Infof("AI分析初始化配置结果: %s", response)
423  		if err == nil {
424  			type Item struct {
425  				Title string `json:"title"`
426  				Desc  string `json:"desc"`
427  			}
428  			var items []Item
429  			data := models.GetJsonString(response)
430  			err = json.Unmarshal([]byte(data), &items)
431  			if err != nil {
432  				callbacks.StepStatusUpdateCallback(step01, statusId01, AgentStatusCompleted, texts.initConfig, "")
433  				callbacks.StepStatusUpdateCallback(step01, uuid.NewString(), AgentStatusCompleted, texts.targetConfig, fmt.Sprintf(texts.targetCountTemplate, len(targets)))
434  			}
435  			if len(items) > 0 {
436  				callbacks.StepStatusUpdateCallback(step01, statusId01, AgentStatusCompleted, items[0].Title, items[0].Desc)
437  				if len(items) >= 2 {
438  					for _, item := range items[1 : len(items)-1] {
439  						s1 := uuid.NewString()
440  						//callbacks.StepStatusUpdateCallback(step01, s1, AgentStatusRunning, "思考中", "AI思考中")
441  						//time.Sleep(time.Millisecond * 600)
442  						callbacks.StepStatusUpdateCallback(step01, s1, AgentStatusCompleted, item.Title, item.Desc)
443  					}
444  				}
445  			}
446  		}
447  	} else {
448  		callbacks.StepStatusUpdateCallback(step01, statusId01, AgentStatusCompleted, texts.initConfig, "")
449  		callbacks.StepStatusUpdateCallback(step01, uuid.NewString(), AgentStatusCompleted, texts.targetConfig, fmt.Sprintf(texts.targetCountTemplate, len(targets)))
450  	}
451  
452  	// 更新任务计划
453  	tasks[0].Status = SubTaskStatusDone
454  	tasks[1].Status = SubTaskStatusDoing
455  	tasks[1].StartedAt = time.Now().Unix()
456  	callbacks.PlanUpdateCallback(tasks)
457  
458  	// 步骤2: 执行扫描
459  	step02 := tasks[1].StepId
460  	callbacks.NewPlanStepCallback(step02, texts.execScan)
461  	statusId02 := uuid.NewString()
462  	callbacks.StepStatusUpdateCallback(step02, statusId02, AgentStatusCompleted, texts.aigWorking, texts.scanningDesc)
463  
464  	toolId02 := uuid.NewString()
465  	callbacks.ToolUsedCallback(step02, statusId02, texts.execScanTool,
466  		[]Tool{CreateTool(toolId02, texts.aiScannerTool, ToolStatusDoing, texts.execScanDesc, texts.scanOperation, texts.targetSystem, "")})
467  
468  	// 收集扫描结果
469  	scanResults := make([]runner.CallbackScanResult, 0)
470  	mu := sync.Mutex{}
471  
472  	// analysisWg waits for all concurrent AI-analysis / screenshot goroutines to finish
473  	// before we compute the final score and report.
474  	var analysisWg sync.WaitGroup
475  
476  	// analysisSem limits concurrent AI + screenshot goroutines to avoid overwhelming
477  	// the model API or the screenshot service when there are many targets.
478  	const maxConcurrentAnalysis = 5
479  	analysisSem := make(chan struct{}, maxConcurrentAnalysis)
480  
481  	processFunc := func(data interface{}) {
482  		switch v := data.(type) {
483  		case runner.CallbackScanResult:
484  			var log string
485  			var appFinger string
486  			if v.Fingerprint != "" {
487  				appFinger = fmt.Sprintf(texts.webAppTemplate, v.Fingerprint)
488  			}
489  			if len(v.Vulnerabilities) > 0 {
490  				log = fmt.Sprintf(texts.vulnFoundTemplate, v.TargetURL, appFinger, len(v.Vulnerabilities))
491  			} else {
492  				log = fmt.Sprintf(texts.noVulnTemplate, v.TargetURL, appFinger)
493  			}
494  
495  			callbacks.ToolUseLogCallback(toolId02, texts.aiScannerTool, step02, log)
496  
497  			// Run AI analysis and screenshot concurrently, outside the result mutex,
498  			// so that multiple targets are processed in parallel instead of sequentially.
499  			analysisWg.Add(1)
500  			go func(result runner.CallbackScanResult, logMsg string) {
501  				defer analysisWg.Done()
502  
503  				// Acquire semaphore slot
504  				analysisSem <- struct{}{}
505  				defer func() { <-analysisSem }()
506  
507  				// AI模式下的额外分析
508  				if model != nil {
509  					status := uuid.NewString()
510  					callbacks.StepStatusUpdateCallback(step02, status, AgentStatusRunning, texts.scanResult, "AI analysis")
511  
512  					prompt := fmt.Sprintf("这是AI基础设施扫描的扫描结果,请你根据以下文本进行总结和归纳,你最后要补充一句(后面将调用未授权检测工具继续扫描,不需要一模一样的文字,大致意思是这样就可以):'我将进行截图分析,继续探索网页上可能的漏洞点',扫描结果如下:\n%s\n", logMsg)
513  					if request.Language == "en" {
514  						prompt += "## 返回使用全英文"
515  					}
516  					response, _ := model.ChatResponse(context.Background(), prompt)
517  					callbacks.StepStatusUpdateCallback(step02, status, AgentStatusCompleted, texts.scanResult, response)
518  				}
519  
520  				// 截图和AI分析
521  				newUuid := uuid.NewString()
522  				func() {
523  					callbacks.StepStatusUpdateCallback(step02, newUuid, AgentStatusRunning, "A.I.G is Thinking", "")
524  					defer callbacks.StepStatusUpdateCallback(step02, newUuid, AgentStatusCompleted, "A.I.G Finished", "")
525  
526  					var screenshotData []byte
527  					var vulInfo *vulstruct.Info
528  					var summary string
529  					var err error
530  
531  					if model != nil {
532  						screenshotData, vulInfo, summary, err = runner.Analysis(result.TargetURL, result.Resp, request.Language, model)
533  						if err != nil {
534  							gologger.WithError(err).Errorf("AI分析失败: %v", err)
535  							return
536  						}
537  						result.Reason = summary
538  					} else {
539  						screenshotData, err = runner.ScreenShot(result.TargetURL)
540  						if err != nil {
541  							gologger.WithError(err).Errorf("截图失败: %v", err)
542  							return
543  						}
544  					}
545  
546  					if len(screenshotData) > 0 {
547  						tmpPath := path.Join(os.TempDir(), fmt.Sprintf("%d.jpg", time.Now().UnixMicro()))
548  						if err := os.WriteFile(tmpPath, screenshotData, 0644); err != nil {
549  							gologger.WithError(err).Errorf("write file failed: %v", err)
550  							return
551  						}
552  						info, err := utils.UploadFile(t.Server, tmpPath)
553  						if err != nil {
554  							gologger.WithError(err).Errorf("upload file failed: %v", err)
555  							return
556  						}
557  						result.ScreenShot = "/api/v1/images/" + info.Data.FileUrl
558  
559  						if model != nil && vulInfo != nil && (vulInfo.Severity == "high" || vulInfo.Severity == "medium") {
560  							result.Vulnerabilities = append(result.Vulnerabilities, *vulInfo)
561  						}
562  					}
563  
564  					// AI模式生成摘要
565  					if model != nil {
566  						vData, _ := json.Marshal(result.Vulnerabilities)
567  						summaryPrompt := "根据以下我提供的漏洞信息,请总结一下发现x个漏洞,会导致xx业务风险,建议xx修复,几句简短的话概括,若未提供漏洞信息,就说目前暂时无漏洞发现。漏洞信息如下:\n" + string(vData)
568  						if request.Language == "en" {
569  							summaryPrompt += "## 返回使用全英文"
570  						}
571  						summary2, _ := model.ChatResponse(context.Background(), summaryPrompt)
572  						result.Summary = summary2
573  					}
574  				}()
575  
576  				mu.Lock()
577  				scanResults = append(scanResults, result)
578  				mu.Unlock()
579  			}(v, log)
580  
581  		case runner.CallbackErrorInfo:
582  			callbacks.ToolUseLogCallback(toolId02, texts.aiScannerTool, step02, fmt.Sprintf(texts.errorTemplate, v.Target, v.Error))
583  		case runner.CallbackProcessInfo, runner.CallbackReportInfo, runner.Step01:
584  			// 忽略这些类型
585  		default:
586  			gologger.Errorf("processFunc unknown type: %T\n", v)
587  		}
588  	}
589  
590  	opts.SetCallback(processFunc)
591  	r, err := runner.New(opts)
592  	if err != nil {
593  		return fmt.Errorf("new runner failed: %v", err)
594  	}
595  	defer r.Close()
596  
597  	// 执行扫描
598  	r.RunEnumeration()
599  
600  	// Wait for all concurrent AI-analysis / screenshot goroutines to finish
601  	// before computing the final score and generating the report.
602  	analysisWg.Wait()
603  
604  	// 计算安全评分
605  	advies := make([]vulstruct.Info, 0)
606  	for _, item := range scanResults {
607  		advies = append(advies, item.Vulnerabilities...)
608  	}
609  	score := r.CalcSecScore(advies)
610  
611  	callbacks.StepStatusUpdateCallback(step02, statusId02, AgentStatusCompleted, texts.aigCompleted, texts.scanCompleteDesc)
612  	callbacks.ToolUsedCallback(step02, statusId02, texts.execScanTool,
613  		[]Tool{CreateTool(toolId02, texts.aiScannerTool, ToolStatusDone, texts.scanComplete, texts.scanOperation, texts.targetSystem, fmt.Sprintf(texts.scanResultTemplate, len(scanResults)))})
614  	callbacks.StepStatusUpdateCallback(step02, uuid.NewString(), AgentStatusCompleted, texts.aigCompleted, texts.taskCompleteDesc)
615  
616  	// 更新任务计划
617  	tasks[1].Status = SubTaskStatusDone
618  	tasks[2].Status = SubTaskStatusDoing
619  	tasks[2].StartedAt = time.Now().Unix()
620  	callbacks.PlanUpdateCallback(tasks)
621  
622  	// 步骤3: 生成报告
623  	step03 := tasks[2].StepId
624  	callbacks.NewPlanStepCallback(step03, texts.genReport)
625  	statustool := uuid.NewString()
626  	callbacks.StepStatusUpdateCallback(step03, statustool, AgentStatusCompleted, texts.aigWorking, texts.reportGenDesc)
627  
628  	toolId03 := uuid.NewString()
629  	callbacks.ToolUsedCallback(step03, statustool, texts.generateReportTool,
630  		[]Tool{CreateTool(toolId03, texts.reportGeneratorTool, ToolStatusDone, texts.reportGenToolDesc, texts.generateReport, "", fmt.Sprintf("%d", len(scanResults)))})
631  
632  	// 发送最终结果
633  	result := map[string]interface{}{
634  		"total":   len(advies),
635  		"score":   score.SecScore,
636  		"results": scanResults,
637  	}
638  	tasks[2].Status = SubTaskStatusDone
639  	callbacks.PlanUpdateCallback(tasks)
640  	callbacks.ResultCallback(result)
641  
642  	return nil
643  }