Skip to main content
Glama

lint-mcp

by wer8956741
main.go66 kB
package main import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) // buildErrorResult 统一将错误以 JSON Issues 形式返回,避免上层只显示 "Error:" func buildErrorResult(message string) *mcp.CallToolResult { lr := &LintResult{Issues: []Issue{{ FromLinter: "go-guard", Text: message, Pos: Pos{Filename: "system", Line: 0, Column: 0}, }}} b, _ := json.Marshal(lr) return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Type: "text", Text: string(b)}}} } // CodeLintRequest 定义智能代码检查请求结构 type CodeLintRequest struct { Files []string `json:"files" description:"参考文件列表(可选,用于确定检查起点)。工具将自动智能检测当前工作目录的所有变更文件。" required:"false"` ProjectPath string `json:"projectPath" description:"项目根目录(可选,优先作为检测起点,建议为Git仓库或包含go.mod的目录)"` } // CodeReviewRequest 定义代码审查请求结构(简化版) type CodeReviewRequest struct { ProjectPath string `json:"projectPath" description:"项目根目录(可选,优先作为检测起点,建议为Git仓库或包含go.mod的目录)"` ReviewFocus []string `json:"reviewFocus" description:"关注点列表,如 ['concurrency', 'transaction', 'resource', 'performance'],默认全部" required:"false"` } // FileAnalysis 文件分析结果(重构版) type FileAnalysis struct { File string `json:"file"` // 文件路径 ChangedLines []ChangedLine `json:"changedLines"` // 变更行信息 RiskPoints []RiskPoint `json:"riskPoints"` // 风险点列表 RiskLevel string `json:"riskLevel"` // "high", "medium", "low", "none" } // ResourceScope 资源作用域信息(优化版) type ResourceScope struct { ID string `json:"id"` // 唯一标识符 Type string `json:"type"` // "transaction", "mutex", "file", "http_response" StartLine int `json:"startLine"` // 资源开启行号 EndLine int `json:"endLine"` // 资源关闭行号 OpenPattern string `json:"openPattern"` // 开启模式,如 "tx, err := db.Begin()" ClosePattern string `json:"closePattern"` // 关闭模式,如 "tx.Commit()" Variable string `json:"variable"` // 资源变量名,如 "tx", "mu", "file" HasChanges bool `json:"hasChanges"` // 变更是否在此作用域内 RiskPatterns []string `json:"riskPatterns"` // 潜在风险模式 ContextCode string `json:"contextCode"` // 完整的作用域代码 } // ResourcePattern 资源模式定义 type ResourcePattern struct { Type string `json:"Type"` // 资源类型 Name string `json:"Name"` // 资源名称(中文) OpenRegex string `json:"OpenRegex"` // 开启模式正则 CloseRegex string `json:"CloseRegex"` // 关闭模式正则 RiskPatterns []string `json:"RiskPatterns"` // 风险模式描述 } // ChangedLine 变更行信息 type ChangedLine struct { LineNumber int `json:"LineNumber"` // 行号 Content string `json:"Content"` // 行内容 Type string `json:"Type"` // "added", "modified", "deleted" } // ContextBlock 上下文代码块 type ContextBlock struct { Type string `json:"Type"` // 上下文类型,如 "transaction", "function" StartLine int `json:"StartLine"` // 起始行号 EndLine int `json:"EndLine"` // 结束行号 Code string `json:"Code"` // 代码内容 Reason string `json:"Reason"` // 包含此上下文的原因 } // EnhancedContext 增强的代码上下文 type EnhancedContext struct { ChangedLines []ChangedLine `json:"ChangedLines"` // 变更的代码行 ResourceScopes []ResourceScope `json:"ResourceScopes"` // 检测到的资源作用域 ContextBlocks []ContextBlock `json:"ContextBlocks"` // 相关上下文块 ProjectPath string `json:"ProjectPath"` // 项目路径 BaseCommit string `json:"BaseCommit"` // 基准提交 Strategy string `json:"Strategy"` // 检测策略 } // RiskPoint 风险点定义 type RiskPoint struct { ID string `json:"id"` // 唯一标识符 Type string `json:"type"` // 风险类型: "transaction_leak", "panic_risk", "resource_leak" Category string `json:"category"` // 风险分类: "transaction", "concurrency", "panic_safety", "resource" Line int `json:"line"` // 风险行号 Severity string `json:"severity"` // 严重程度: "high", "medium", "low" Description string `json:"description"` // 风险描述 Context string `json:"context"` // 关键代码片段 Suggestion string `json:"suggestion"` // 修复建议 } // AIInstructions AI结构化指令对象(优化版) type AIInstructions struct { Type string `json:"type"` // 指令类型,如 "code_review_prompt" Priority string `json:"priority"` // 优先级: "high", "medium", "low" Action string `json:"action"` // 行动指令: "analyze_and_respond" Prompt string `json:"prompt"` // 精简提示词 FocusAreas []string `json:"focus_areas"` // 关注领域 ExpectedOutput string `json:"expected_output"` // 期望输出格式 Language string `json:"language"` // 编程语言 AnalysisDepth string `json:"analysis_depth"` // 分析深度: "shallow", "medium", "deep" IncludeSuggestions bool `json:"include_suggestions"` // 是否包含修复建议 IncludeExamples bool `json:"include_examples"` // 是否包含代码示例 RiskThreshold string `json:"risk_threshold"` // 风险阈值: "low", "medium", "high" OutputFormat string `json:"output_format"` // 输出格式: "json", "markdown", "structured" ContextType string `json:"context_type"` // 上下文类型 ConfidenceThreshold float64 `json:"confidence_threshold"` // 置信度阈值 } // CodeReviewOutput 代码审查输出结果(重构版 - 专注风险数据) type CodeReviewOutput struct { Summary ReviewSummary `json:"summary"` // 概览信息 ChangedFiles []FileAnalysis `json:"changedFiles"` // 变更文件分析(包含风险点) ProcessingTime string `json:"processingTime"` // 处理时间 } // ReviewSummary 审查概览(重构版 - 专注风险统计) type ReviewSummary struct { TotalChangedFiles int `json:"totalChangedFiles"` // 变更文件数 TotalRiskPoints int `json:"totalRiskPoints"` // 总风险点数 RiskLevel string `json:"riskLevel"` // "high", "medium", "low", "none" RiskByCategory map[string]int `json:"riskByCategory"` // 按分类统计风险: {"transaction": 2, "panic_safety": 1} RiskBySeverity map[string]int `json:"riskBySeverity"` // 按严重程度统计: {"high": 1, "medium": 2} ProcessingTime string `json:"processingTime"` // 处理时间 } // GolangciLintOutput golangci-lint 的实际输出格式 type GolangciLintOutput struct { Issues []Issue `json:"Issues"` Report struct { Linters []struct { Name string `json:"Name"` Enabled bool `json:"Enabled"` } `json:"Linters"` } `json:"Report"` } // LintResult 表示代码检查结果 type LintResult struct { Issues []Issue `json:"Issues"` } // Issue 表示单个代码问题 type Issue struct { FromLinter string `json:"FromLinter"` Text string `json:"Text"` Severity string `json:"Severity"` SourceLines []string `json:"SourceLines"` Replacement *Replacement `json:"Replacement"` Pos Pos `json:"Pos"` ExpectNoLint bool `json:"ExpectNoLint"` ExpectedNoLintLinter string `json:"ExpectedNoLintLinter"` } type Replacement struct { NewLines []string `json:"NewLines"` } type Pos struct { Filename string `json:"Filename"` Offset int `json:"Offset"` Line int `json:"Line"` Column int `json:"Column"` } // findGoModRoot 从指定目录开始向上查找go.mod文件 func findGoModRoot(startDir string) (string, error) { dir := startDir for { goModPath := filepath.Join(dir, "go.mod") if _, err := os.Stat(goModPath); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { break // 已经到达根目录 } dir = parent } return "", fmt.Errorf("未找到go.mod文件") } // getProjectRootFromFile 从文件路径获取项目根目录 func getProjectRootFromFile(filePath string) (string, error) { if !filepath.IsAbs(filePath) { return "", fmt.Errorf("文件路径必须是绝对路径: %s", filePath) } // 从文件所在目录开始向上查找go.mod searchDir := filepath.Dir(filePath) log.Printf("从文件 %s 开始搜索项目根目录,搜索目录: %s", filePath, searchDir) if goModRoot, err := findGoModRoot(searchDir); err == nil { log.Printf("找到Go模块根目录: %s", goModRoot) return goModRoot, nil } // 如果没有找到go.mod,使用文件所在目录作为项目根目录 log.Printf("未找到go.mod文件,使用文件所在目录作为项目根目录: %s", searchDir) return searchDir, nil } // getPackagesFromFiles 从文件列表中获取包路径列表(去重) func getPackagesFromFiles(files []string) (map[string][]string, error) { // 返回 map[projectRoot][]packagePaths 的结构 projectPackages := make(map[string]map[string]bool) result := make(map[string][]string) for _, file := range files { if file == "" { continue } if !filepath.IsAbs(file) { return nil, fmt.Errorf("文件路径必须是绝对路径: %s", file) } // 检查文件是否存在且是.go文件 if _, err := os.Stat(file); err != nil { log.Printf("警告:文件 %s 不存在,跳过", file) continue } if !strings.HasSuffix(file, ".go") { log.Printf("警告:文件 %s 不是Go文件,跳过", file) continue } // 获取项目根目录 projectRoot, err := getProjectRootFromFile(file) if err != nil { return nil, fmt.Errorf("获取项目根目录失败: %v", err) } // 获取文件所在目录作为包路径 packageDir := filepath.Dir(file) // 转换为相对于项目根目录的包路径 relPackagePath, err := filepath.Rel(projectRoot, packageDir) if err != nil { log.Printf("警告:无法计算包路径 %s 相对于项目根目录 %s 的路径:%v", packageDir, projectRoot, err) continue } // 在项目根目录的情况下使用 "." if relPackagePath == "" || relPackagePath == "." { relPackagePath = "." } else { // 确保使用正斜杠(Go模块路径格式) relPackagePath = strings.ReplaceAll(relPackagePath, "\\", "/") // 如果路径不以./开头,添加它 if !strings.HasPrefix(relPackagePath, "./") { relPackagePath = "./" + relPackagePath } } // 按项目根目录分组包路径 if projectPackages[projectRoot] == nil { projectPackages[projectRoot] = make(map[string]bool) } if !projectPackages[projectRoot][relPackagePath] { projectPackages[projectRoot][relPackagePath] = true log.Printf("发现包: %s (项目: %s, 文件: %s)", relPackagePath, projectRoot, file) } } // 转换为最终结果格式 for projectRoot, packages := range projectPackages { var packageList []string for pkg := range packages { packageList = append(packageList, pkg) } result[projectRoot] = packageList } if len(result) == 0 { return nil, fmt.Errorf("没有找到有效的Go包") } return result, nil } // getGolangciLintPath 获取可用的golangci-lint路径 func getGolangciLintPath() (string, error) { // 优先尝试GOPATH/bin中的版本(通常是go install安装的) if gopath := os.Getenv("GOPATH"); gopath != "" { gopathBin := filepath.Join(gopath, "bin", "golangci-lint") if _, err := os.Stat(gopathBin); err == nil { return gopathBin, nil } } // 尝试go env GOPATH cmd := exec.Command("go", "env", "GOPATH") if output, err := cmd.Output(); err == nil { gopath := strings.TrimSpace(string(output)) if gopath != "" { gopathBin := filepath.Join(gopath, "bin", "golangci-lint") if _, err := os.Stat(gopathBin); err == nil { return gopathBin, nil } } } // 最后尝试PATH中的版本 if path, err := exec.LookPath("golangci-lint"); err == nil { return path, nil } return "", fmt.Errorf("golangci-lint 未找到") } // checkGolangciLintVersion 检查golangci-lint版本是否符合要求 func checkGolangciLintVersion(golangciLintPath string) error { cmd := exec.Command(golangciLintPath, "--version") output, err := cmd.Output() if err != nil { return fmt.Errorf("无法获取golangci-lint版本: %v", err) } versionOutput := string(output) log.Printf("检测到golangci-lint版本: %s", strings.TrimSpace(versionOutput)) // 检查版本兼容性 cmd = exec.Command(golangciLintPath, "run", "--help") helpOutput, err := cmd.Output() if err != nil { return fmt.Errorf("无法获取golangci-lint run帮助信息: %v", err) } helpStr := string(helpOutput) // 检查是否支持JSON输出 // v2.4.0+支持--output.json.path(新版本格式) supportsNewJsonOutput := strings.Contains(helpStr, "--output.json.path") // v1.52.2+支持--out-format json(旧版本格式) supportsOldJsonOutput := strings.Contains(helpStr, "--out-format") if !supportsNewJsonOutput && !supportsOldJsonOutput { return fmt.Errorf(`golangci-lint 版本不兼容 当前版本: %s 要求版本: v1.52.2 或更高版本 请升级到兼容版本: # 推荐版本(适合Go 1.20+) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 # 或最新版本(适合Go 1.21+) go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 升级后请重启MCP服务。`, strings.TrimSpace(versionOutput)) } return nil } // getGolangciLintOutputArgs 根据版本获取JSON输出参数 func getGolangciLintOutputArgs() ([]string, error) { golangciLintPath, err := getGolangciLintPath() if err != nil { return nil, err } cmd := exec.Command(golangciLintPath, "run", "--help") helpOutput, err := cmd.Output() if err != nil { return nil, fmt.Errorf("无法获取golangci-lint run帮助信息: %v", err) } helpStr := string(helpOutput) // v2.4.0+支持--output.json.path(优先检查新版本) if strings.Contains(helpStr, "--output.json.path") { return []string{"--output.json.path", "stdout"}, nil } // v1.52.2+支持--out-format(向后兼容) if strings.Contains(helpStr, "--out-format") { return []string{"--out-format", "json", "--print-issued-lines=false", "--print-linter-name=true"}, nil } return nil, fmt.Errorf("不支持的golangci-lint版本") } // checkGolangciLintInstalled 检查golangci-lint是否已安装并且版本兼容 func checkGolangciLintInstalled() error { golangciLintPath, err := getGolangciLintPath() if err != nil { return err } // 检查版本兼容性 return checkGolangciLintVersion(golangciLintPath) } // autoDetectVendorMode 自动检测项目的依赖模式 func autoDetectVendorMode(projectRoot string) bool { log.Printf("检测项目 %s 的vendor模式", projectRoot) // 规则:仅依据项目根目录 .gitignore 是否包含完整的 'vendor/' 目录忽略 // - 存在 'vendor/' 完整目录忽略 -> modules 模式(返回 false) // - 不存在完整目录忽略 -> vendor 模式(返回 true) gitignorePath := filepath.Join(projectRoot, ".gitignore") content, err := os.ReadFile(gitignorePath) if err != nil { log.Printf("无法读取 .gitignore 文件: %v,默认使用 vendor 模式", err) return true } lines := strings.Split(string(content), "\n") for _, raw := range lines { line := strings.TrimSpace(raw) if line == "" || strings.HasPrefix(line, "#") { continue } // 精确匹配:只有完整的 vendor/ 目录忽略才是 modules 模式 // 兼容 '/vendor/' 与 'vendor/' 等写法,但不匹配 vendor/xxx/yyy.go 这种具体文件 clean := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/") if clean == "vendor" { log.Printf(".gitignore 含完整 vendor/ 目录忽略(行: %s),使用 modules 模式", line) return false } } log.Printf(".gitignore 未包含完整 vendor/ 目录忽略,使用 vendor 模式") return true } // findAllGoFiles 在指定目录下查找所有Go文件(备用策略) func findAllGoFiles(projectRoot string) ([]string, error) { log.Printf("扫描目录中的所有Go文件: %s", projectRoot) var goFiles []string err := filepath.Walk(projectRoot, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 跳过vendor目录和隐藏目录 if info.IsDir() && (info.Name() == "vendor" || strings.HasPrefix(info.Name(), ".")) { return filepath.SkipDir } // 只处理Go文件 if !info.IsDir() && strings.HasSuffix(info.Name(), ".go") { // 跳过测试文件和生成的文件 if !strings.HasSuffix(info.Name(), "_test.go") && !strings.Contains(info.Name(), ".pb.go") && !strings.Contains(info.Name(), ".gen.go") { absPath, err := filepath.Abs(path) if err == nil { goFiles = append(goFiles, absPath) log.Printf("找到Go文件: %s", absPath) } } } return nil }) if err != nil { return nil, fmt.Errorf("扫描目录失败: %v", err) } if len(goFiles) == 0 { return nil, fmt.Errorf("目录中没有找到Go文件") } log.Printf("总共扫描到 %d 个Go文件", len(goFiles)) return goFiles, nil } // detectBaseCommit 智能检测基准提交点 func detectBaseCommit(projectRoot string) (string, string, error) { log.Printf("智能检测项目 %s 的基准提交点", projectRoot) // 直接尝试各策略,若命令失败则跳过到下一策略 // 策略1: 检测未推送的提交 log.Printf("策略1: 尝试检测未推送的提交...") cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") cmd.Dir = projectRoot branchOutput, err := cmd.Output() if err == nil { currentBranch := strings.TrimSpace(string(branchOutput)) log.Printf("当前分支: %s", currentBranch) remoteBranches := []string{"origin/" + currentBranch, "upstream/" + currentBranch, "remote/" + currentBranch} for _, remoteBranch := range remoteBranches { cmd = exec.Command("git", "rev-parse", "--verify", remoteBranch) cmd.Dir = projectRoot if err := cmd.Run(); err == nil { cmd = exec.Command("git", "rev-list", "--count", remoteBranch+"..HEAD") cmd.Dir = projectRoot countOutput, err := cmd.Output() if err == nil { count := strings.TrimSpace(string(countOutput)) if count != "0" { return remoteBranch, fmt.Sprintf("未推送的提交(%s个)", count), nil } } } } } // 策略2: 检测与主分支的分叉点 log.Printf("策略2: 尝试检测与主分支的分叉点...") mainBranch := getActualMainBranch(projectRoot) if mainBranch != "" { cmd = exec.Command("git", "merge-base", "HEAD", mainBranch) cmd.Dir = projectRoot output, err := cmd.Output() if err == nil { mergeBase := strings.TrimSpace(string(output)) cmd = exec.Command("git", "rev-list", "--count", mergeBase+"..HEAD") cmd.Dir = projectRoot countOutput, err := cmd.Output() if err == nil { count := strings.TrimSpace(string(countOutput)) if count != "0" { log.Printf("✅ 找到与%s的分叉点: %s (%s个提交)", mainBranch, mergeBase, count) return mergeBase, fmt.Sprintf("分支分叉点(vs %s, %s个提交)", mainBranch, count), nil } } } } else { log.Printf("⚠️ 未找到有效主分支,跳过策略2") } // 策略3: 工作区变更 cmd = exec.Command("git", "status", "--porcelain") cmd.Dir = projectRoot statusOutput, err := cmd.Output() if err == nil && len(strings.TrimSpace(string(statusOutput))) > 0 { return "", "工作区变更", nil } // 策略4: 最近几次提交 for i := 2; i <= 5; i++ { base := fmt.Sprintf("HEAD~%d", i) cmd = exec.Command("git", "rev-parse", "--verify", base) cmd.Dir = projectRoot if err := cmd.Run(); err == nil { return base, fmt.Sprintf("最近%d次提交", i), nil } } return "HEAD~1", "最近一次提交", nil } // getChangedGoFiles 获取变更的 Go 文件列表(工作区 + 提交范围并集) func getChangedGoFiles(projectRoot string) ([]string, string, string, error) { // 尝试检测基准提交点(用于提交范围) baseCommit, strategy, _ := detectBaseCommit(projectRoot) log.Printf("使用检测策略: %s,基准提交: %s", strategy, baseCommit) changedSet := make(map[string]struct{}) addLines := func(lines string) { for _, raw := range strings.Split(strings.TrimSpace(lines), "\n") { line := strings.TrimSpace(raw) if line == "" || !strings.HasSuffix(line, ".go") { continue } var absPath string if filepath.IsAbs(line) { absPath = line } else { absPath = filepath.Join(projectRoot, line) } cleanPath := filepath.Clean(absPath) finalPath, err := filepath.Abs(cleanPath) if err != nil { continue } if _, err := os.Stat(finalPath); err == nil { changedSet[finalPath] = struct{}{} } } } run := func(args ...string) string { cmd := exec.Command("git", args...) cmd.Dir = projectRoot out, err := cmd.Output() if err != nil { log.Printf("git %v 失败: %v", args, err) return "" } return string(out) } // 1) 工作区未暂存 addLines(run("diff", "--name-only")) // 2) 工作区已暂存 addLines(run("diff", "--name-only", "--cached")) // 3) 未跟踪的新文件 addLines(run("ls-files", "--others", "--exclude-standard")) // 4) 提交范围(若存在) if baseCommit != "" { addLines(run("diff", "--name-only", baseCommit, "HEAD")) } // 汇总 var changedGoFiles []string for p := range changedSet { changedGoFiles = append(changedGoFiles, p) log.Printf("收集到变更 Go 文件: %s", p) } if len(changedGoFiles) == 0 { return nil, "", "", fmt.Errorf("未找到任何变更的 Go 文件(工作区与提交范围均为空)") } log.Printf("总共找到 %d 个变更的 Go 文件(工作区+提交范围)", len(changedGoFiles)) return changedGoFiles, baseCommit, strategy, nil } // runGolangciLintForChangedPackages 对包含变更文件的包进行智能检查 func runGolangciLintForChangedPackages(projectRoot string, changedFiles []string, baseCommit string, vendorMode bool) (*LintResult, error) { log.Printf("开始包级智能检查,项目根目录: %s,基准提交: %s,变更文件数: %d", projectRoot, baseCommit, len(changedFiles)) // 按包分组变更文件 packageDirs := make(map[string]bool) for _, file := range changedFiles { packageDir := filepath.Dir(file) packageDirs[packageDir] = true } log.Printf("发现 %d 个包含变更的包", len(packageDirs)) allIssues := make([]Issue, 0) // 对每个包进行检查 for packageDir := range packageDirs { relPackageDir, err := filepath.Rel(projectRoot, packageDir) if err != nil { log.Printf("警告:无法计算包路径 %s 相对于项目根目录 %s 的路径:%v", packageDir, projectRoot, err) continue } if relPackageDir == "" || relPackageDir == "." { relPackageDir = "." } else { // 确保使用正斜杠(Go模块路径格式) relPackageDir = strings.ReplaceAll(relPackageDir, "\\", "/") // 如果路径不以./开头,添加它 if !strings.HasPrefix(relPackageDir, "./") { relPackageDir = "./" + relPackageDir } } log.Printf("检查包: %s (目录: %s)", relPackageDir, packageDir) args := []string{"run"} if vendorMode { args = append(args, "--modules-download-mode=vendor") } // 关键:添加智能检测的基准提交 if baseCommit != "" { args = append(args, "--new-from-rev", baseCommit) log.Printf("使用基准提交进行变更检测: %s", baseCommit) } // 根据版本获取JSON输出参数 outputArgs, err := getGolangciLintOutputArgs() if err != nil { log.Printf("包 %s 获取输出参数失败: %v", relPackageDir, err) continue } args = append(args, outputArgs...) args = append(args, relPackageDir) result, err := runGolangciLintWithArgs(projectRoot, args) if err != nil { log.Printf("包 %s 检查失败: %v", relPackageDir, err) continue } if result != nil && len(result.Issues) > 0 { log.Printf("包 %s 发现 %d 个问题", relPackageDir, len(result.Issues)) allIssues = append(allIssues, result.Issues...) } else { log.Printf("包 %s 检查通过,无问题", relPackageDir) } } log.Printf("包级检查完成,总共发现 %d 个问题", len(allIssues)) return &LintResult{Issues: allIssues}, nil } // runGolangciLintWithArgs 以自定义参数运行 golangci-lint 并解析 JSON 结果 func runGolangciLintWithArgs(projectRoot string, args []string) (*LintResult, error) { log.Printf("执行命令: golangci-lint %v", args) log.Printf("命令执行目录: %s", projectRoot) // golangci-lint 可用性检查已在服务启动时完成 golangciLintPath, err := getGolangciLintPath() if err != nil { log.Printf("golangci-lint 未找到: %v", err) return nil, fmt.Errorf("golangci-lint 未找到: %v", err) } cmd := exec.Command(golangciLintPath, args...) cmd.Dir = projectRoot cmd.Env = os.Environ() output, cmdErr := cmd.CombinedOutput() log.Printf("命令输出长度: %d", len(output)) log.Printf("命令执行错误: %v", cmdErr) // 即使有命令错误,也尝试解析输出(golangci-lint 发现问题时会返回非零退出码) if len(output) == 0 { if cmdErr != nil { log.Printf("命令无输出且有错误: %v", cmdErr) return nil, fmt.Errorf("golangci-lint 执行失败: %v", cmdErr) } return &LintResult{Issues: make([]Issue, 0)}, nil } jsonOutput := extractJSONFromOutput(string(output)) if jsonOutput == "" { log.Printf("未能从输出中提取有效JSON,原始输出: %s", string(output)) return &LintResult{Issues: make([]Issue, 0)}, nil } var golangciOutput GolangciLintOutput if err := json.Unmarshal([]byte(jsonOutput), &golangciOutput); err != nil { log.Printf("JSON解析失败: %v,JSON内容: %s", err, jsonOutput) return &LintResult{Issues: make([]Issue, 0)}, nil } log.Printf("成功解析到 %d 个问题", len(golangciOutput.Issues)) return &LintResult{Issues: golangciOutput.Issues}, nil } // extractJSONFromOutput 从golangci-lint输出中提取JSON部分 func extractJSONFromOutput(output string) string { log.Printf("原始输出内容: %s", output) // 如果输出为空,直接返回 trimmedOutput := strings.TrimSpace(output) if trimmedOutput == "" { log.Printf("输出为空") return "" } // 如果输出以 { 开始且以 } 结束,可能是完整的JSON if strings.HasPrefix(trimmedOutput, "{") && strings.HasSuffix(trimmedOutput, "}") { log.Printf("输出似乎已经是完整的JSON") // 验证是否为有效的JSON var js map[string]interface{} if err := json.Unmarshal([]byte(trimmedOutput), &js); err == nil { return trimmedOutput } log.Printf("看似JSON但解析失败,继续尝试提取") } // 尝试提取JSON部分 log.Printf("尝试从输出中提取JSON部分") // 分行处理,查找可能的JSON行 lines := strings.Split(output, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "{") && strings.HasSuffix(line, "}") { // 验证是否为有效的JSON var js map[string]interface{} if err := json.Unmarshal([]byte(line), &js); err == nil { log.Printf("找到有效的JSON行") return line } } } // 如果没有找到完整的JSON行,尝试提取最长的JSON片段 maxLength := 0 bestJSON := "" for i, char := range output { if char == '{' { // 找到一个开始位置 currentStart := i braceCount := 1 // 向后查找匹配的结束括号 for j := i + 1; j < len(output); j++ { if output[j] == '{' { braceCount++ } else if output[j] == '}' { braceCount-- if braceCount == 0 { // 找到一个完整的JSON片段 length := j - currentStart + 1 if length > maxLength { // 验证是否为有效的JSON candidate := output[currentStart : j+1] var js map[string]interface{} if err := json.Unmarshal([]byte(candidate), &js); err == nil { maxLength = length bestJSON = candidate } } break } } } } } if bestJSON != "" { log.Printf("找到最长的有效JSON片段,长度: %d", len(bestJSON)) return bestJSON } // 如果仍然没有找到有效的JSON,返回空字符串 log.Printf("未找到有效的JSON") return "" } // handleCodeLintRequest 处理智能代码检查请求 func handleCodeLintRequest(ctx context.Context, req mcp.CallToolRequest) (result *mcp.CallToolResult, err error) { // 捕获任何 panic 并转换为错误返回 defer func() { if r := recover(); r != nil { log.Printf("❌ 发生 panic: %v", r) result = buildErrorResult(fmt.Sprintf("内部错误: %v", r)) err = nil // MCP 框架期望错误在结果中,而不是返回错误 } }() log.Printf("🚀 收到智能代码检查请求: name=%s", req.Params.Name) log.Printf("📋 请求参数: %+v", req.Params.Arguments) var lintReq CodeLintRequest // 将 arguments 映射解码到结构体 if req.Params.Arguments == nil { req.Params.Arguments = map[string]interface{}{} } argsBytes, _ := json.Marshal(req.Params.Arguments) if err := json.Unmarshal(argsBytes, &lintReq); err != nil { return buildErrorResult(fmt.Sprintf("无效的请求参数: %v", err)), nil } log.Printf("解析后的请求: %+v", lintReq) // 如果既没有 projectPath 也没有 files,则直接给出明确指引,避免从可执行目录误扫系统盘 if strings.TrimSpace(lintReq.ProjectPath) == "" && (len(lintReq.Files) == 0 || strings.TrimSpace(lintReq.Files[0]) == "") { msg := "缺少项目起点:请提供 projectPath(项目根目录绝对路径,推荐)或 files(任一项目内文件的绝对路径)。例如:{\"projectPath\":\"/Users/you/path/to/project\"}。" return buildErrorResult(msg), nil } // 计算检测起点目录:优先 projectPath -> files 推断 -> 当前工作目录 baseDir := "" if lintReq.ProjectPath != "" { abs, err := filepath.Abs(lintReq.ProjectPath) if err != nil { return buildErrorResult(fmt.Sprintf("projectPath 解析失败: %v", err)), nil } if !filepath.IsAbs(abs) { return buildErrorResult("projectPath 必须是绝对路径"), nil } if stat, err := os.Stat(abs); err != nil || !stat.IsDir() { return buildErrorResult(fmt.Sprintf("projectPath 无效或不是目录: %s", abs)), nil } baseDir = abs } else if len(lintReq.Files) > 0 && lintReq.Files[0] != "" { root, err := getProjectRootFromFile(lintReq.Files[0]) if err != nil { return buildErrorResult(fmt.Sprintf("从 files 推断项目根目录失败: %v", err)), nil } baseDir = root } else { cwd, err := os.Getwd() if err != nil { return buildErrorResult(fmt.Sprintf("获取当前工作目录失败: %v", err)), nil } abs, err := filepath.Abs(cwd) if err != nil { return buildErrorResult(fmt.Sprintf("转换绝对路径失败: %v", err)), nil } baseDir = abs } log.Printf("检测起点目录: %s", baseDir) // 开始智能变更检测和包级检查 log.Printf("开始智能检测变更文件(起点: %s)", baseDir) // 获取最新变更的 Go 文件(工作区+提交范围) changedFiles, baseCommit, strategy, err := getChangedGoFiles(baseDir) if err != nil { log.Printf("Git检测失败(起点: %s),尝试备用策略: %v", baseDir, err) fallbackFiles, fallbackErr := findAllGoFiles(baseDir) if fallbackErr != nil { return buildErrorResult(fmt.Sprintf("Git检测失败(起点: %s): %v\n备用文件扫描也失败: %v\n\n请提供 projectPath 或 files 以明确项目位置。", baseDir, err, fallbackErr)), nil } log.Printf("使用备用策略:扫描到 %d 个Go文件(起点: %s)", len(fallbackFiles), baseDir) changedFiles = fallbackFiles baseCommit = "" // 备用策略时不使用基准提交 strategy = "备用文件扫描" } log.Printf("检测策略: %s,基准提交: %s,变更文件: %d个", strategy, baseCommit, len(changedFiles)) // 按项目分组变更文件,因为变更可能涉及多个项目 projectFiles := make(map[string][]string) for _, file := range changedFiles { projectRoot, err := getProjectRootFromFile(file) if err != nil { log.Printf("警告:无法确定文件 %s 的项目根目录:%v", file, err) continue } projectFiles[projectRoot] = append(projectFiles[projectRoot], file) } // 对每个项目进行包级智能检查 allIssues := make([]Issue, 0) for projectRoot, files := range projectFiles { log.Printf("检查项目 %s 中的 %d 个变更文件", projectRoot, len(files)) vendorMode := autoDetectVendorMode(projectRoot) result, err := runGolangciLintForChangedPackages(projectRoot, files, baseCommit, vendorMode) if err != nil { log.Printf("项目 %s 检查失败: %v", projectRoot, err) return buildErrorResult(fmt.Sprintf("代码检查失败\n项目: %s\n基准提交: %s\nvendorMode: %v\n错误: %v", projectRoot, baseCommit, vendorMode, err)), nil } if result != nil { allIssues = append(allIssues, result.Issues...) } } log.Printf("所有项目检查完成,总共发现 %d 个问题", len(allIssues)) finalResult := &LintResult{Issues: allIssues} resultJSON, _ := json.Marshal(finalResult) return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Type: "text", Text: string(resultJSON)}}}, nil } // getActualMainBranch 获取项目实际使用的主分支 func getActualMainBranch(projectRoot string) string { log.Printf("🔍 智能检测实际主分支...") // 方法1: 检查Git默认分支配置 (最准确) cmd := exec.Command("git", "symbolic-ref", "refs/remotes/origin/HEAD") cmd.Dir = projectRoot if output, err := cmd.Output(); err == nil { defaultRef := strings.TrimSpace(string(output)) if parts := strings.Split(defaultRef, "/"); len(parts) >= 3 { branchName := strings.Join(parts[3:], "/") remoteBranch := "origin/" + branchName // 验证该分支是否真实存在 if verifyBranchExists(projectRoot, remoteBranch) { log.Printf("✅ 检测到Git默认分支: %s", remoteBranch) return remoteBranch } // 尝试本地分支 if verifyBranchExists(projectRoot, branchName) { log.Printf("✅ 检测到Git默认分支(本地): %s", branchName) return branchName } } } // 方法2: reflog历史检测 (检查当前分支是从哪里checkout出来的) currentBranch := getCurrentBranchSmart(projectRoot) if currentBranch != "" { cmd := exec.Command("git", "reflog", "--oneline", "-n", "15") cmd.Dir = projectRoot if output, err := cmd.Output(); err == nil { lines := strings.Split(strings.TrimSpace(string(output)), "\n") for _, line := range lines { if strings.Contains(line, "checkout: moving from") && strings.Contains(line, " to "+currentBranch) { if idx := strings.Index(line, "moving from "); idx >= 0 { remaining := line[idx+len("moving from "):] if toIdx := strings.Index(remaining, " to "); toIdx >= 0 { sourceBranch := remaining[:toIdx] if sourceBranch != currentBranch && sourceBranch != "" { // 优先检查origin/分支 remoteBranch := "origin/" + sourceBranch if verifyBranchExists(projectRoot, remoteBranch) { log.Printf("✅ 从reflog发现源分支: %s", remoteBranch) return remoteBranch } // 检查本地分支 if verifyBranchExists(projectRoot, sourceBranch) { log.Printf("✅ 从reflog发现源分支(本地): %s", sourceBranch) return sourceBranch } } } } } } } } // 方法3: 按优先级检查常见主分支 candidates := []string{"origin/main", "main", "origin/master", "master", "origin/develop", "develop"} for _, branch := range candidates { if verifyBranchExists(projectRoot, branch) { log.Printf("✅ 找到存在的主分支: %s", branch) return branch } } log.Printf("⚠️ 未能找到有效的主分支") return "" } // verifyBranchExists 验证分支是否存在 func verifyBranchExists(projectRoot, branch string) bool { cmd := exec.Command("git", "rev-parse", "--verify", branch) cmd.Dir = projectRoot return cmd.Run() == nil } // getCurrentBranchSmart 获取当前分支名 func getCurrentBranchSmart(projectRoot string) string { cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") cmd.Dir = projectRoot if output, err := cmd.Output(); err == nil { return strings.TrimSpace(string(output)) } return "" } // ==================== AI代码审查功能 ==================== // 支持的资源模式定义 var SupportedResourcePatterns = []ResourcePattern{ { Type: "transaction", Name: "数据库事务", OpenRegex: `(\w+),\s*err\s*:=\s*\w*\.Begin\w*\(`, CloseRegex: `(\w+)\.(?:Commit|Rollback)\(\)`, RiskPatterns: []string{ "事务开启后未正确关闭", "错误路径中缺少事务回滚", "事务嵌套导致死锁", "长时间持有事务连接", }, }, { Type: "mutex", Name: "互斥锁", OpenRegex: `(\w+)\.(?:Lock|RLock)\(\)`, CloseRegex: `(\w+)\.(?:Unlock|RUnlock)\(\)`, RiskPatterns: []string{ "锁获取后未释放", "错误路径中缺少锁释放", "锁的获取和释放不匹配", "可能导致死锁的锁顺序", }, }, { Type: "file", Name: "文件句柄", OpenRegex: `(\w+),\s*err\s*:=\s*(?:os\.Open|os\.Create|ioutil\.ReadFile)\(`, CloseRegex: `(\w+)\.Close\(\)`, RiskPatterns: []string{ "文件打开后未关闭", "错误路径中文件句柄泄漏", "defer语句缺失", "文件句柄重复关闭", }, }, { Type: "http_response", Name: "HTTP响应", OpenRegex: `(\w+),\s*err\s*:=\s*http\.(?:Get|Post|Do)\(`, CloseRegex: `(\w+)\.Body\.Close\(\)`, RiskPatterns: []string{ "HTTP响应体未关闭", "连接池资源泄漏", "defer语句缺失", "响应体重复关闭", }, }, { Type: "context", Name: "上下文取消", OpenRegex: `(\w+),\s*(\w+)\s*:=\s*context\.WithCancel\(`, CloseRegex: `(\w+)\(\)`, RiskPatterns: []string{ "上下文取消函数未调用", "goroutine泄漏风险", "资源清理不完整", "超时控制失效", }, }, { Type: "sql_rows", Name: "数据库查询结果", OpenRegex: `(\w+),\s*err\s*:=\s*\w*\.Query\w*\(`, CloseRegex: `(\w+)\.Close\(\)`, RiskPatterns: []string{ "查询结果未关闭导致连接泄漏", "defer rows.Close()缺失", "错误路径中结果集未关闭", "连接池耗尽风险", }, }, { Type: "grpc_conn", Name: "gRPC连接", OpenRegex: `(\w+),\s*err\s*:=\s*grpc\.Dial\w*\(`, CloseRegex: `(\w+)\.Close\(\)`, RiskPatterns: []string{ "gRPC连接未关闭", "连接资源泄漏", "defer conn.Close()缺失", "服务端连接数过多", }, }, { Type: "temp_dir", Name: "临时目录", OpenRegex: `(\w+),\s*err\s*:=\s*(?:ioutil\.TempDir|os\.MkdirTemp)\(`, CloseRegex: `os\.RemoveAll\((\w+)\)`, RiskPatterns: []string{ "临时目录未清理", "磁盘空间泄漏", "defer清理语句缺失", "系统临时目录堆积", }, }, { Type: "redis_conn", Name: "Redis连接", OpenRegex: `(\w+)\s*:=\s*\w*\.Get\(\)`, CloseRegex: `(\w+)\.Close\(\)`, RiskPatterns: []string{ "Redis连接未归还连接池", "连接池资源耗尽", "defer conn.Close()缺失", "连接超时未处理", }, }, { Type: "channel", Name: "消息队列通道", OpenRegex: `(\w+),\s*err\s*:=\s*\w*\.Channel\(\)`, CloseRegex: `(\w+)\.Close\(\)`, RiskPatterns: []string{ "消息通道未关闭", "AMQP连接资源泄漏", "defer ch.Close()缺失", "消息队列连接数过多", }, }, { Type: "zip_reader", Name: "压缩文件读取", OpenRegex: `(\w+),\s*err\s*:=\s*zip\.OpenReader\(`, CloseRegex: `(\w+)\.Close\(\)`, RiskPatterns: []string{ "压缩文件读取器未关闭", "文件描述符泄漏", "defer reader.Close()缺失", "大文件处理内存泄漏", }, }, { Type: "ticker", Name: "定时器", OpenRegex: `(\w+)\s*:=\s*time\.NewTicker\(`, CloseRegex: `(\w+)\.Stop\(\)`, RiskPatterns: []string{ "定时器未停止", "goroutine泄漏风险", "defer ticker.Stop()缺失", "内存持续增长", }, }, { Type: "buffer_pool", Name: "缓冲池", OpenRegex: `(\w+)\s*:=\s*\w*\.Get\(\)`, CloseRegex: `\w*\.Put\((\w+)\)`, RiskPatterns: []string{ "缓冲区未归还池中", "内存池效率降低", "defer pool.Put()缺失", "内存使用不当", }, }, { Type: "waitgroup", Name: "等待组", OpenRegex: `(\w+)\.Add\(\d+\)`, CloseRegex: `(\w+)\.Done\(\)`, RiskPatterns: []string{ "WaitGroup计数不匹配", "goroutine永久阻塞", "defer wg.Done()缺失", "并发控制失效", }, }, { Type: "panic_recovery", Name: "Panic恢复机制", OpenRegex: `defer\s+func\(\)\s*\{[^}]*recover\(\)`, CloseRegex: `\}\(\)`, RiskPatterns: []string{ "panic未被正确捕获", "recover()调用位置不当", "错误信息丢失", "程序异常终止风险", }, }, { Type: "slice_bounds", Name: "切片边界检查", OpenRegex: `if\s+len\((\w+)\)\s*[><=]+\s*\d+`, CloseRegex: `(\w+)\[.*\]`, RiskPatterns: []string{ "数组/切片越界访问", "边界检查缺失", "panic: index out of range", "运行时崩溃风险", }, }, { Type: "type_assertion", Name: "类型断言安全", OpenRegex: `(\w+),\s*(\w+)\s*:=\s*.*\.\(.*\)`, CloseRegex: `if\s+!(\w+)`, RiskPatterns: []string{ "类型断言失败导致panic", "ok检查缺失", "类型转换不安全", "运行时类型错误", }, }, { Type: "nil_pointer", Name: "空指针检查", OpenRegex: `if\s+(\w+)\s*!=\s*nil`, CloseRegex: `(\w+)\..*`, RiskPatterns: []string{ "空指针解引用", "nil检查缺失", "panic: runtime error", "指针访问不安全", }, }, } // detectResourceScopes 检测代码中的资源作用域 func detectResourceScopes(fileContent string, changedLines []int) ([]ResourceScope, error) { lines := strings.Split(fileContent, "\n") var scopes []ResourceScope for _, pattern := range SupportedResourcePatterns { foundScopes := findResourceScopes(lines, pattern, changedLines) scopes = append(scopes, foundScopes...) } log.Printf("检测到 %d 个资源作用域", len(scopes)) return scopes, nil } // findResourceScopes 查找特定模式的资源作用域 func findResourceScopes(lines []string, pattern ResourcePattern, changedLines []int) []ResourceScope { var scopes []ResourceScope openRegex := regexp.MustCompile(pattern.OpenRegex) closeRegex := regexp.MustCompile(pattern.CloseRegex) for i, line := range lines { if matches := openRegex.FindStringSubmatch(line); matches != nil { variable := "" if len(matches) > 1 { variable = matches[1] // 提取变量名 } // 寻找对应的关闭语句 closeLineIdx := findMatchingClose(lines, i, variable, closeRegex) if closeLineIdx > i { scope := ResourceScope{ Type: pattern.Type, StartLine: i + 1, EndLine: closeLineIdx + 1, OpenPattern: strings.TrimSpace(line), ClosePattern: strings.TrimSpace(lines[closeLineIdx]), Variable: variable, } // 检查变更是否在此作用域内 for _, changedLine := range changedLines { if changedLine > scope.StartLine && changedLine < scope.EndLine { scope.HasChanges = true break } } scopes = append(scopes, scope) log.Printf("发现%s作用域: %d-%d行, 变量: %s, 包含变更: %v", pattern.Name, scope.StartLine, scope.EndLine, scope.Variable, scope.HasChanges) } } } return scopes } // findMatchingClose 查找匹配的关闭语句(找到最后一个匹配的) func findMatchingClose(lines []string, startIdx int, variable string, closeRegex *regexp.Regexp) int { lastMatch := -1 for i := startIdx + 1; i < len(lines); i++ { line := lines[i] if matches := closeRegex.FindStringSubmatch(line); matches != nil { // 如果有变量名,检查是否匹配 if variable != "" && len(matches) > 1 { if matches[1] == variable { lastMatch = i } } else { // 没有变量名或无法提取变量名,直接匹配模式 lastMatch = i } } } return lastMatch } // extractChangedLines 从Git diff中提取变更行信息 func extractChangedLines(filePath, baseCommit string) ([]ChangedLine, error) { var changedLines []ChangedLine // 读取当前文件内容 fileContent, err := ioutil.ReadFile(filePath) if err != nil { return changedLines, fmt.Errorf("读取文件失败: %v", err) } fileLines := strings.Split(string(fileContent), "\n") // 获取文件的Git diff cmd := exec.Command("git", "diff", baseCommit, "HEAD", "--unified=3", filePath) cmd.Dir = filepath.Dir(filePath) output, err := cmd.Output() if err != nil { // 如果Git diff失败,尝试检查工作区变更 cmd = exec.Command("git", "diff", "--unified=3", filePath) cmd.Dir = filepath.Dir(filePath) output, err = cmd.Output() if err != nil { return changedLines, fmt.Errorf("无法获取文件diff: %v", err) } } // 解析diff输出,提取变更行 lines := strings.Split(string(output), "\n") for i, line := range lines { if strings.HasPrefix(line, "@@") { // 解析行号范围,如 @@ -10,5 +10,7 @@ re := regexp.MustCompile(`@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@`) matches := re.FindStringSubmatch(line) if len(matches) >= 2 { startLine, _ := strconv.Atoi(matches[1]) // 解析后续的diff行,提取实际变更内容 for j := i + 1; j < len(lines); j++ { diffLine := lines[j] if strings.HasPrefix(diffLine, "@@") { break // 遇到下一个diff块 } if strings.HasPrefix(diffLine, "+") && !strings.HasPrefix(diffLine, "+++") { // 新增行 content := strings.TrimPrefix(diffLine, "+") lineNum := startLine + len(changedLines) changedLines = append(changedLines, ChangedLine{ LineNumber: lineNum, Content: content, Type: "added", }) } else if strings.HasPrefix(diffLine, "-") && !strings.HasPrefix(diffLine, "---") { // 删除行(记录但不计入行号) content := strings.TrimPrefix(diffLine, "-") changedLines = append(changedLines, ChangedLine{ LineNumber: startLine, Content: content, Type: "deleted", }) } else if strings.HasPrefix(diffLine, " ") { // 上下文行,跳过但调整行号计数 startLine++ } } } } } // 如果diff解析失败,回退到简单的行号范围检测 if len(changedLines) == 0 { lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.HasPrefix(line, "@@") { re := regexp.MustCompile(`@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@`) matches := re.FindStringSubmatch(line) if len(matches) >= 2 { startLine, _ := strconv.Atoi(matches[1]) count := 1 if len(matches) > 2 && matches[2] != "" { count, _ = strconv.Atoi(matches[2]) } // 从文件中读取实际内容 for i := 0; i < count; i++ { lineNum := startLine + i content := "" if lineNum > 0 && lineNum <= len(fileLines) { content = fileLines[lineNum-1] } changedLines = append(changedLines, ChangedLine{ LineNumber: lineNum, Content: content, Type: "modified", }) } } } } } log.Printf("文件 %s 检测到 %d 个变更行", filePath, len(changedLines)) return changedLines, nil } // buildEnhancedContext 构建增强的代码上下文 func buildEnhancedContext(filePath, baseCommit, strategy string) (*EnhancedContext, error) { // 读取文件内容 fileContent, err := ioutil.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("读取文件失败: %v", err) } // 提取变更行 changedLines, err := extractChangedLines(filePath, baseCommit) if err != nil { return nil, fmt.Errorf("提取变更行失败: %v", err) } // 提取行号列表 lineNumbers := make([]int, len(changedLines)) for i, change := range changedLines { lineNumbers[i] = change.LineNumber } // 检测资源作用域 scopes, err := detectResourceScopes(string(fileContent), lineNumbers) if err != nil { return nil, fmt.Errorf("检测资源作用域失败: %v", err) } // 构建上下文 context := &EnhancedContext{ ChangedLines: changedLines, ResourceScopes: scopes, ProjectPath: filepath.Dir(filePath), BaseCommit: baseCommit, Strategy: strategy, } // 为在资源作用域内的变更构建上下文块 for _, scope := range scopes { if scope.HasChanges { scopeCode := extractLines(string(fileContent), scope.StartLine, scope.EndLine) context.ContextBlocks = append(context.ContextBlocks, ContextBlock{ Type: scope.Type, StartLine: scope.StartLine, EndLine: scope.EndLine, Code: scopeCode, Reason: fmt.Sprintf("变更代码位于%s作用域内", scope.Type), }) } } log.Printf("构建增强上下文完成: %d个变更行, %d个资源作用域, %d个上下文块", len(context.ChangedLines), len(context.ResourceScopes), len(context.ContextBlocks)) return context, nil } // extractLines 提取指定行范围的代码 func extractLines(content string, startLine, endLine int) string { lines := strings.Split(content, "\n") if startLine < 1 || endLine > len(lines) || startLine > endLine { return "" } var result []string for i := startLine - 1; i < endLine; i++ { result = append(result, fmt.Sprintf("%d: %s", i+1, lines[i])) } return strings.Join(result, "\n") } // ==================== 智能上下文分析 ==================== // assessRiskLevel 评估风险等级 func assessRiskLevel(scopes []ResourceScope, changedLines []ChangedLine) string { highRiskCount := 0 mediumRiskCount := 0 lowRiskCount := 0 // 基于资源作用域评估 for _, scope := range scopes { if scope.HasChanges { switch scope.Type { case "transaction", "mutex", "waitgroup": highRiskCount++ case "sql_rows", "grpc_conn", "redis_conn": highRiskCount++ case "file", "http_response", "channel": mediumRiskCount++ case "context", "ticker", "zip_reader": mediumRiskCount++ case "temp_dir", "buffer_pool": lowRiskCount++ } } } // 基于变更行数量和内容评估 if len(changedLines) > 50 { mediumRiskCount++ // 大量变更增加风险 } // 检查变更内容中的高风险关键词 for _, line := range changedLines { content := strings.ToLower(line.Content) // 高风险:panic、数组越界、空指针、并发问题 if strings.Contains(content, "panic") || strings.Contains(content, "unsafe") || strings.Contains(content, "goroutine") || strings.Contains(content, "go func") || strings.Contains(content, "[") && strings.Contains(content, "]") || // 数组/切片访问 strings.Contains(content, "*") && strings.Contains(content, ".") || // 指针解引用 strings.Contains(content, ".(") || // 类型断言 strings.Contains(content, "must") || // MustXxx函数 strings.Contains(content, "fatal") { highRiskCount++ } else if strings.Contains(content, "defer") || strings.Contains(content, "close") || strings.Contains(content, "commit") || strings.Contains(content, "rollback") || strings.Contains(content, "recover") || // panic恢复 strings.Contains(content, "len(") || // 长度检查 strings.Contains(content, "cap(") || // 容量检查 strings.Contains(content, "nil") { // nil检查 mediumRiskCount++ } } if highRiskCount > 0 { return "high" } if mediumRiskCount > 0 { return "medium" } if lowRiskCount > 0 || len(changedLines) > 10 { return "low" } return "none" } // identifyFocusAreas 识别需要重点关注的领域 func identifyFocusAreas(scopes []ResourceScope) []string { focusMap := make(map[string]bool) for _, scope := range scopes { if scope.HasChanges { switch scope.Type { case "transaction": focusMap["transaction"] = true case "mutex", "waitgroup": focusMap["concurrency"] = true case "file", "http_response", "grpc_conn", "redis_conn", "channel", "zip_reader", "temp_dir", "buffer_pool": focusMap["resource"] = true case "context", "ticker": focusMap["concurrency"] = true case "panic_recovery", "slice_bounds", "type_assertion", "nil_pointer": focusMap["panic_safety"] = true case "sql_rows": focusMap["transaction"] = true focusMap["resource"] = true } } } var areas []string for area := range focusMap { areas = append(areas, area) } return areas } // generateSimplifiedPrompt 生成精简的AI提示词模板(已废弃) func generateSimplifiedPrompt(files []FileAnalysis, scopes []ResourceScope, focusAreas []string) string { return "代码审查工具已重构,不再生成提示词" } // 已删除不再需要的函数 // detectRiskPoints 检测代码风险点 func detectRiskPoints(filePath string, changedLines []ChangedLine, scopes []ResourceScope) []RiskPoint { var riskPoints []RiskPoint riskID := 1 // 1. 基于变更行内容检测风险 for _, line := range changedLines { content := strings.TrimSpace(line.Content) if content == "" { continue } contentLower := strings.ToLower(content) // 检测各种风险模式 risks := []struct { pattern string riskType string category string severity string description string suggestion string }{ // Panic 风险 {"panic(", "panic_call", "panic_safety", "high", "直接调用panic可能导致程序崩溃", "使用错误返回值替代panic,或添加recover机制"}, {"must", "panic_risk", "panic_safety", "medium", "MustXxx函数可能触发panic", "检查函数文档,添加错误处理"}, {"[", "bounds_risk", "panic_safety", "medium", "数组/切片访问可能越界", "添加边界检查: if index < len(arr)"}, {".(", "type_assertion", "panic_safety", "medium", "类型断言失败可能panic", "使用安全的类型断言: val, ok := x.(Type)"}, // 事务风险 {"begin(", "transaction_start", "transaction", "high", "事务开启,需确保正确关闭", "添加defer tx.Rollback()或在成功时调用tx.Commit()"}, {"commit(", "transaction_commit", "transaction", "medium", "事务提交,需确保错误处理", "检查commit返回的错误"}, {"rollback(", "transaction_rollback", "transaction", "low", "事务回滚操作", "确保在适当的错误路径中调用"}, // 资源风险 {"open(", "resource_open", "resource", "medium", "资源打开,需确保关闭", "添加defer file.Close()"}, {"close()", "resource_close", "resource", "low", "资源关闭操作", "确保资源正确关闭"}, // 并发风险 {"go func", "goroutine_start", "concurrency", "medium", "启动goroutine,注意资源管理", "确保goroutine能正常退出,避免泄漏"}, {"lock()", "mutex_lock", "concurrency", "medium", "获取锁,需确保释放", "添加defer mu.Unlock()"}, {"unlock()", "mutex_unlock", "concurrency", "low", "释放锁操作", "确保锁的获取和释放匹配"}, } for _, risk := range risks { if strings.Contains(contentLower, risk.pattern) { riskPoint := RiskPoint{ ID: fmt.Sprintf("%s_%d_%d", risk.riskType, line.LineNumber, riskID), Type: risk.riskType, Category: risk.category, Line: line.LineNumber, Severity: risk.severity, Description: risk.description, Context: content, Suggestion: risk.suggestion, } riskPoints = append(riskPoints, riskPoint) riskID++ } } } // 2. 基于资源作用域检测风险 for _, scope := range scopes { if scope.HasChanges { var riskType, description, suggestion string severity := "medium" switch scope.Type { case "transaction": riskType = "transaction_scope_change" description = "事务作用域内有代码变更,需检查事务完整性" suggestion = "确保变更不会影响事务的正确提交或回滚" case "mutex": riskType = "mutex_scope_change" description = "锁作用域内有代码变更,需检查并发安全" suggestion = "确保变更不会引入竞态条件或死锁" severity = "high" case "panic_recovery": riskType = "panic_recovery_change" description = "panic恢复机制内有变更,需检查错误处理" suggestion = "确保panic能被正确捕获和处理" severity = "high" default: riskType = "resource_scope_change" description = fmt.Sprintf("%s资源作用域内有变更", scope.Type) suggestion = "检查资源的正确获取和释放" } riskPoint := RiskPoint{ ID: fmt.Sprintf("%s_%s_%d", riskType, scope.ID, riskID), Type: riskType, Category: getCategoryByType(scope.Type), Line: scope.StartLine, Severity: severity, Description: description, Context: fmt.Sprintf("%s作用域 (第%d-%d行)", scope.Type, scope.StartLine, scope.EndLine), Suggestion: suggestion, } riskPoints = append(riskPoints, riskPoint) riskID++ } } return riskPoints } // getCategoryByType 根据类型获取分类 func getCategoryByType(scopeType string) string { switch scopeType { case "transaction", "sql_rows": return "transaction" case "mutex", "waitgroup", "context", "ticker": return "concurrency" case "panic_recovery", "slice_bounds", "type_assertion", "nil_pointer": return "panic_safety" default: return "resource" } } // assessFileRiskLevel 评估单个文件的风险等级 func assessFileRiskLevel(riskPoints []RiskPoint) string { if len(riskPoints) == 0 { return "none" } highCount := 0 mediumCount := 0 for _, risk := range riskPoints { switch risk.Severity { case "high": highCount++ case "medium": mediumCount++ } } if highCount > 0 { return "high" } if mediumCount > 0 { return "medium" } return "low" } // 已删除 generateAIInstructions 函数(不再需要) // ==================== 主处理函数 ==================== // handleCodeReview 处理代码审查请求(重构版 - 专注风险检测) func handleCodeReview(request *CodeReviewRequest) (*mcp.CallToolResult, error) { startTime := time.Now() log.Printf("开始代码审查分析,项目路径: %s", request.ProjectPath) // 1. 获取变更的Go文件 changedFiles, baseCommit, _, err := getChangedGoFiles(request.ProjectPath) if err != nil { return nil, fmt.Errorf("获取变更文件失败: %v", err) } if len(changedFiles) == 0 { log.Printf("未检测到Go文件变更") return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{ Type: "text", Text: `{"summary":{"totalChangedFiles":0,"totalRiskPoints":0,"riskLevel":"none"},"changedFiles":[],"processingTime":"0ms"}`, }}, }, nil } // 2. 分析每个变更文件,检测风险点 var allFileAnalyses []FileAnalysis totalRiskPoints := 0 for _, filePath := range changedFiles { log.Printf("分析文件: %s", filePath) // 获取变更行 changedLines, err := extractChangedLines(filePath, baseCommit) if err != nil { log.Printf("提取文件 %s 的变更行失败: %v", filePath, err) continue } // 如果没有变更行,跳过 if len(changedLines) == 0 { log.Printf("文件 %s 无变更行", filePath) continue } // 检测资源作用域 fileContent, err := ioutil.ReadFile(filePath) if err != nil { log.Printf("读取文件 %s 失败: %v", filePath, err) continue } changedLineNumbers := make([]int, len(changedLines)) for i, line := range changedLines { changedLineNumbers[i] = line.LineNumber } scopes, err := detectResourceScopes(string(fileContent), changedLineNumbers) if err != nil { log.Printf("检测文件 %s 的资源作用域失败: %v", filePath, err) scopes = []ResourceScope{} // 继续处理,但不包含作用域 } // 检测风险点 riskPoints := detectRiskPoints(filePath, changedLines, scopes) // 评估文件风险等级 riskLevel := assessFileRiskLevel(riskPoints) // 创建文件分析结果 fileAnalysis := FileAnalysis{ File: filePath, ChangedLines: changedLines, RiskPoints: riskPoints, RiskLevel: riskLevel, } allFileAnalyses = append(allFileAnalyses, fileAnalysis) totalRiskPoints += len(riskPoints) log.Printf("文件 %s 分析完成,发现 %d 个风险点", filePath, len(riskPoints)) } // 3. 创建概览摘要(专注风险统计) riskByCategory := make(map[string]int) riskBySeverity := make(map[string]int) overallRiskLevel := "none" // 统计风险点 for _, file := range allFileAnalyses { for _, risk := range file.RiskPoints { riskByCategory[risk.Category]++ riskBySeverity[risk.Severity]++ } // 更新整体风险等级 if file.RiskLevel == "high" { overallRiskLevel = "high" } else if file.RiskLevel == "medium" && overallRiskLevel != "high" { overallRiskLevel = "medium" } else if file.RiskLevel == "low" && overallRiskLevel == "none" { overallRiskLevel = "low" } } summary := ReviewSummary{ TotalChangedFiles: len(allFileAnalyses), TotalRiskPoints: totalRiskPoints, RiskLevel: overallRiskLevel, RiskByCategory: riskByCategory, RiskBySeverity: riskBySeverity, ProcessingTime: time.Since(startTime).String(), } // 4. 构建最终结果(精简版) result := CodeReviewOutput{ Summary: summary, ChangedFiles: allFileAnalyses, ProcessingTime: time.Since(startTime).String(), } log.Printf("代码审查分析完成,处理 %d 个文件,发现 %d 个风险点,处理时间: %s", len(allFileAnalyses), totalRiskPoints, result.ProcessingTime) // 5. 返回结果 resultJSON, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("序列化结果失败: %v", err) } return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{Type: "text", Text: string(resultJSON)}}, }, nil } func main() { // 设置日志输出到文件 logFile, err := os.OpenFile("/tmp/go-guard-debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { // 如果无法创建日志文件,使用stderr log.SetOutput(os.Stderr) } else { log.SetOutput(logFile) defer logFile.Close() } // 设置日志输出格式,包含时间和文件位置 log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("🚀 启动 go-guard 服务 (兼容版本)...") log.Printf("📝 日志文件: /tmp/go-guard-debug.log") log.Printf("🕐 启动时间: %s", time.Now().Format("2006-01-02 15:04:05")) s := server.NewMCPServer( "go-guard", "1.0.16", ) // 注册 code_lint 工具 tool := mcp.NewTool("code_lint", mcp.WithDescription("智能Go代码检查工具。自动检测变更代码并进行精确的包级行级检查,避免历史代码问题干扰。支持多项目处理和智能依赖模式检测。"), mcp.WithString("projectPath", mcp.Description("项目根目录(可选,优先作为检测起点,建议为Git仓库或包含go.mod的目录)"), ), ) s.AddTool(tool, handleCodeLintRequest) log.Println("工具注册成功: code_lint") // 注册 code_review 工具 reviewTool := mcp.NewTool("code_review", mcp.WithDescription("智能代码上下文分析工具。基于变更检测,识别资源作用域,为外部AI工具提供精准的代码上下文和结构化分析数据。专注于并发、事务、资源管理等关键领域。"), mcp.WithString("projectPath", mcp.Description("项目根目录(可选,优先作为检测起点,建议为Git仓库或包含go.mod的目录)"), ), mcp.WithArray("reviewFocus", mcp.Description("关注点列表,可选值:concurrency(并发安全)、transaction(事务管理)、resource(资源管理)、performance(性能问题)。默认全部"), ), ) s.AddTool(reviewTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { var request CodeReviewRequest // 手动解析参数 if projectPath, ok := req.Params.Arguments["projectPath"].(string); ok { request.ProjectPath = projectPath } if reviewFocus, ok := req.Params.Arguments["reviewFocus"].([]interface{}); ok { for _, rf := range reviewFocus { if rfStr, ok := rf.(string); ok { request.ReviewFocus = append(request.ReviewFocus, rfStr) } } } return handleCodeReview(&request) }) log.Println("工具注册成功: code_review") // 启动时检查golangci-lint版本兼容性 if err := checkGolangciLintInstalled(); err != nil { log.Printf("❌ golangci-lint 版本检查失败: %v", err) log.Println("请按照提示升级golangci-lint后重启服务") return } log.Println("✅ golangci-lint 版本检查通过") log.Println("服务就绪,等待连接...") if err := server.ServeStdio(s); err != nil { log.Printf("服务错误: %v\n", err) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wer8956741/mcpCodeCheck'

If you have feedback or need assistance with the MCP directory API, please join our Discord server