feat: 初始化 git-air AI 代码评审 CLI 工具
新增多模型 LLM 客户端、Git diff 提取与过滤、配置管理及 pre-commit 钩子支持。
This commit is contained in:
commit
55f40e015d
18 files changed
+1315
No files matched your search
@@ -0,0 +1,51 @@
|
||||
package reviewer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultBaseSystemPrompt 默认内置的资深架构师评审准则
|
||||
const DefaultBaseSystemPrompt = `你是一位拥有 15 年高并发系统设计经验的资深架构师与代码安全专家。
|
||||
你的任务是对开发者提交的 Git Diff 增量代码进行工业级、严苛、精炼的代码评审(Code Review)。
|
||||
### 评审原则:
|
||||
1. 严禁任何客套话与无意义吹捧,直奔技术实质。
|
||||
2. 聚焦于逻辑缺陷、并发竞态、安全漏洞、内存与协程泄漏、异常处理等实质性问题,不要纠结纯粹的代码缩进或格式。
|
||||
3. 发现具体问题时,必须直接给出修改前后的对比例子或修复代码块。
|
||||
4. 如果代码质量优秀且无缺陷,直接返回 "LGTM: 代码质量良好,未发现明显缺陷",不要强行挑刺。
|
||||
### 重点审查维度:
|
||||
- 并发与竞态安全:Goroutine 泄露、Channel 阻塞、锁竞争与未释放、共享变量无锁并发读写。
|
||||
- 安全与权限:SQL 注入风险、敏感密钥/Token 硬编码、未校验的外部输入、未做权限校验。
|
||||
- 资源与错误处理:连接/文件未 defer Close()、错误被盲目下划线 _ 忽略、Context 未向后传递。
|
||||
- 性能与边界:大循环内的频繁内存分配或数据库查询、切片越界、空指针异常(NIL Dereference)。
|
||||
### 输出格式规范(使用清晰严谨的纯文本标签):
|
||||
#### 变更概述
|
||||
(用 1-2 句话总结本次代码改动核心内容)
|
||||
#### 详细审查意见
|
||||
(如果有问题,按严重程度列出;若无问题则省略此段)
|
||||
- [BLOCKER] 文件路径:行号 - 问题描述及潜在事故危害(必须附带修复代码块)
|
||||
- [WARNING] 文件路径:行号 - 潜在隐患或性能优化点
|
||||
- [SUGGESTION] 文件路径:行号 - 更优雅的 Idiomatic 写法
|
||||
#### 评审结论
|
||||
- 结论: [PASS / WARN / REJECT]
|
||||
- 评分: X / 100
|
||||
`
|
||||
|
||||
// BuildSystemPrompt 构建最终 Prompt(合并 Base Prompt 与 rules.go 加载的项目规则)
|
||||
func BuildSystemPrompt(customPrompt string, repoRoot string) string {
|
||||
var builder strings.Builder
|
||||
// 1. 如果用户指定了自定义 Base Prompt 则使用,否则使用默认内置 Prompt
|
||||
basePrompt := strings.TrimSpace(customPrompt)
|
||||
if basePrompt == "" {
|
||||
basePrompt = DefaultBaseSystemPrompt
|
||||
}
|
||||
|
||||
builder.WriteString(basePrompt)
|
||||
// 2. 调用 rules.go 读取项目专属规则
|
||||
customRules := LoadCustomRules(repoRoot)
|
||||
if customRules != "" {
|
||||
builder.WriteString("\n\n### 团队专属规范(最高优先级准则):\n")
|
||||
builder.WriteString(customRules)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package reviewer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git-air/internal/llm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Reviewer struct {
|
||||
client *llm.Client
|
||||
repoRoot string
|
||||
customPrompt string
|
||||
}
|
||||
|
||||
// NewReviewer 初始化评审引擎
|
||||
// customPrompt 可传入用户自定义配置的 Prompt,传空字符串则使用默认 Prompt
|
||||
func NewReviewer(client *llm.Client, repoRoot string, customPrompt string) *Reviewer {
|
||||
return &Reviewer{
|
||||
client: client,
|
||||
repoRoot: repoRoot,
|
||||
customPrompt: customPrompt,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行代码评审
|
||||
func (r *Reviewer) Execute(ctx context.Context, diff string, stream bool, onChunk func(chunk string)) (string, error) {
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
return "", fmt.Errorf("diff 内容为空,无需评审")
|
||||
}
|
||||
|
||||
// 动态组装最终 System Prompt
|
||||
systemPrompt := BuildSystemPrompt(r.customPrompt, r.repoRoot)
|
||||
if stream && onChunk != nil {
|
||||
err := r.client.ReviewStream(ctx, systemPrompt, diff, onChunk)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.client.Review(ctx, systemPrompt, diff)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package reviewer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RuleFileCandidates 支持的团队规则文件名优先级列表
|
||||
var RuleFileCandidates = []string{
|
||||
".airules",
|
||||
".git-air-rules",
|
||||
".git-air.yaml",
|
||||
".git-air.md",
|
||||
}
|
||||
|
||||
// LoadCustomRules 读取项目根目录下的团队专属规则
|
||||
func LoadCustomRules(repoRoot string) string {
|
||||
if repoRoot == "" {
|
||||
return ""
|
||||
}
|
||||
rulePath := FindRuleFile(repoRoot)
|
||||
if rulePath == "" {
|
||||
return ""
|
||||
}
|
||||
data, err := os.ReadFile(rulePath)
|
||||
if err != nil || len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// FindRuleFile 探测当前仓库根目录下是否存在匹配的规则文件,返回其绝对路径
|
||||
func FindRuleFile(repoRoot string) string {
|
||||
for _, filename := range RuleFileCandidates {
|
||||
fullPath := filepath.Join(repoRoot, filename)
|
||||
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
|
||||
return fullPath
|
||||
}
|
||||
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user