feat: 添加 Token 费用估算与自定义忽略规则
- 在 LLM 客户端中捕获流式响应的 Token 使用量 - 内置多模型官方定价表,支持自定义输入/输出单价覆盖 - 新增 `--price-input` 和 `--price-output` 配置项 - 支持通过 `.airignore` 文件自定义排除评审文件 - 更新中英日文文档,补充费用估算与忽略规则说明
This commit is contained in:
1 parent
02e2e4edd3
commit
26b0342934
12 files changed
+658
-63
No files matched your search
@@ -43,6 +43,9 @@ func GetDiff(ctx context.Context, args ...string) (string, error) {
|
||||
return "", ErrNotGitRepo
|
||||
}
|
||||
|
||||
repoRoot, _ := GetRepoRoot(ctx)
|
||||
customIgnores := LoadAirIgnore(repoRoot)
|
||||
|
||||
var cmdArgs []string
|
||||
if len(args) == 0 {
|
||||
// 1. 默认先提取暂存区代码 (准备 commit 的代码)
|
||||
@@ -51,7 +54,7 @@ func GetDiff(ctx context.Context, args ...string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(stagedDiff) != "" {
|
||||
return truncateDiffIfNeeded(FilterDiff(stagedDiff)), nil
|
||||
return truncateDiffIfNeeded(FilterDiff(stagedDiff, customIgnores...)), nil
|
||||
}
|
||||
// 2. 如果暂存区为空,回退提取工作区未暂存的修改
|
||||
cmdArgs = []string{}
|
||||
@@ -64,7 +67,7 @@ func GetDiff(ctx context.Context, args ...string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filteredDiff := FilterDiff(rawDiff)
|
||||
filteredDiff := FilterDiff(rawDiff, customIgnores...)
|
||||
if strings.TrimSpace(filteredDiff) == "" {
|
||||
return "", ErrEmptyDiff
|
||||
}
|
||||
|
||||
+72
-14
@@ -1,13 +1,14 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 默认需要忽略的文件名与后缀列表
|
||||
var ignoredPatterns = []string{
|
||||
var defaultIgnoredPatterns = []string{
|
||||
// 依赖与锁文件
|
||||
"go.sum",
|
||||
"package-lock.json",
|
||||
@@ -36,19 +37,45 @@ var ignoredPatterns = []string{
|
||||
"*.pdf",
|
||||
}
|
||||
|
||||
// isIgnoredFile 判断文件路径是否应该在 Code Review 中被忽略
|
||||
func isIgnoredFile(filePath string) bool {
|
||||
cleanPath := filepath.ToSlash(filePath)
|
||||
// LoadAirIgnore 从仓库根目录加载 .airignore 文件中的自定义忽略规则
|
||||
func LoadAirIgnore(repoRoot string) []string {
|
||||
if repoRoot == "" {
|
||||
return nil
|
||||
}
|
||||
ignoreFile := filepath.Join(repoRoot, ".airignore")
|
||||
file, err := os.Open(ignoreFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var patterns []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
trimmed := strings.TrimSpace(scanner.Text())
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
patterns = append(patterns, trimmed)
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
// isIgnoredFile 判断文件路径是否命中忽略规则(内置规则 + 用户自定义规则)
|
||||
func isIgnoredFile(filePath string, customPatterns []string) bool {
|
||||
cleanPath := filepath.ToSlash(strings.TrimSpace(filePath))
|
||||
baseName := filepath.Base(cleanPath)
|
||||
|
||||
for _, pattern := range ignoredPatterns {
|
||||
// 精确匹配文件名(如 go.sum)
|
||||
if pattern == baseName {
|
||||
// 1. 先匹配用户自定义规则 (.airignore)
|
||||
for _, pattern := range customPatterns {
|
||||
if matchPattern(pattern, cleanPath, baseName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 通配符匹配(如 *.pb.go)
|
||||
if matched, _ := filepath.Match(pattern, baseName); matched {
|
||||
// 2. 再匹配内置默认规则
|
||||
for _, pattern := range defaultIgnoredPatterns {
|
||||
if matchPattern(pattern, cleanPath, baseName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -56,10 +83,41 @@ func isIgnoredFile(filePath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var diffHeaderRegex = regexp.MustCompile(`(?m)^diff --git a/(.*?) b/(.*?)$`)
|
||||
// matchPattern 单条规则智能匹配(支持目录/、通配符*、精确匹配)
|
||||
func matchPattern(pattern, cleanPath, baseName string) bool {
|
||||
pattern = filepath.ToSlash(strings.TrimSpace(pattern))
|
||||
if pattern == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// FilterDiff 过滤掉 diff 中无意义的锁文件和自动生成文件的区块
|
||||
func FilterDiff(rawDiff string) string {
|
||||
// 目录规则:如 "docs/" 或 "vendor/"
|
||||
if strings.HasSuffix(pattern, "/") {
|
||||
dirPrefix := strings.TrimSuffix(pattern, "/")
|
||||
if strings.HasPrefix(cleanPath, dirPrefix+"/") || cleanPath == dirPrefix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 精确匹配文件名或相对路径
|
||||
if pattern == baseName || pattern == cleanPath {
|
||||
return true
|
||||
}
|
||||
|
||||
// 通配符匹配文件名(如 *.pb.go)
|
||||
if matched, _ := filepath.Match(pattern, baseName); matched {
|
||||
return true
|
||||
}
|
||||
|
||||
// 通配符匹配全路径(如 mock/*.go)
|
||||
if matched, _ := filepath.Match(pattern, cleanPath); matched {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FilterDiff 过滤掉 diff 中命中忽略规则(如锁文件、生成的代码、.airignore 排除项)的文件区块
|
||||
func FilterDiff(rawDiff string, customPatterns ...string) string {
|
||||
if strings.TrimSpace(rawDiff) == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -85,7 +143,7 @@ func FilterDiff(rawDiff string) string {
|
||||
parts := strings.Fields(firstLine)
|
||||
if len(parts) >= 2 {
|
||||
filePath := strings.TrimPrefix(parts[1], "b/")
|
||||
if isIgnoredFile(filePath) {
|
||||
if isIgnoredFile(filePath, customPatterns) {
|
||||
continue // 命中忽略规则,跳过这个文件的 diff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsIgnoredFile(t *testing.T) {
|
||||
customPatterns := []string{
|
||||
"docs/",
|
||||
"tests/mock/*.json",
|
||||
"secrets.env",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
filePath string
|
||||
expected bool
|
||||
}{
|
||||
// 默认内置忽略规则
|
||||
{"go.sum", true},
|
||||
{"package-lock.json", true},
|
||||
{"api/user.pb.go", true},
|
||||
{"web/dist/app.min.js", true},
|
||||
{"assets/logo.png", true},
|
||||
|
||||
// 用户自定义 .airignore 规则
|
||||
{"docs/readme.md", true},
|
||||
{"docs/api/v1.md", true},
|
||||
{"tests/mock/user.json", true},
|
||||
{"secrets.env", true},
|
||||
|
||||
// 不应被忽略的正常业务代码
|
||||
{"cmd/root.go", false},
|
||||
{"internal/service/user.go", false},
|
||||
{"pkg/utils/helper.go", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := isIgnoredFile(tt.filePath, customPatterns)
|
||||
if got != tt.expected {
|
||||
t.Errorf("isIgnoredFile(%q) = %v, expected %v", tt.filePath, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAirIgnore(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "git-air-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("创建临时目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
content := `# 忽略文档
|
||||
docs/
|
||||
# 忽略临时文件
|
||||
*.tmp
|
||||
*.log
|
||||
`
|
||||
ignoreFile := filepath.Join(tempDir, ".airignore")
|
||||
if err := os.WriteFile(ignoreFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("写入 .airignore 失败: %v", err)
|
||||
}
|
||||
|
||||
patterns := LoadAirIgnore(tempDir)
|
||||
expected := []string{"docs/", "*.tmp", "*.log"}
|
||||
|
||||
if len(patterns) != len(expected) {
|
||||
t.Fatalf("LoadAirIgnore 返回数量不符: got %v, expected %v", patterns, expected)
|
||||
}
|
||||
|
||||
for i, p := range patterns {
|
||||
if p != expected[i] {
|
||||
t.Errorf("Pattern[%d] = %q, expected %q", i, p, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterDiff(t *testing.T) {
|
||||
rawDiff := `diff --git a/go.sum b/go.sum
|
||||
index 111..222 100644
|
||||
--- a/go.sum
|
||||
+++ b/go.sum
|
||||
@@ -1 +1 @@
|
||||
+some-package v1.0.0 h1:...
|
||||
diff --git a/internal/service/user.go b/internal/service/user.go
|
||||
index 333..444 100644
|
||||
--- a/internal/service/user.go
|
||||
+++ b/internal/service/user.go
|
||||
@@ -1 +1 @@
|
||||
+func HandleUser() {}
|
||||
diff --git a/docs/spec.md b/docs/spec.md
|
||||
index 555..666 100644
|
||||
--- a/docs/spec.md
|
||||
+++ b/docs/spec.md
|
||||
@@ -1 +1 @@
|
||||
+# API Spec
|
||||
`
|
||||
customPatterns := []string{"docs/"}
|
||||
filtered := FilterDiff(rawDiff, customPatterns...)
|
||||
|
||||
if strings.Contains(filtered, "go.sum") {
|
||||
t.Errorf("FilterDiff 应当过滤掉 go.sum")
|
||||
}
|
||||
if strings.Contains(filtered, "docs/spec.md") {
|
||||
t.Errorf("FilterDiff 应当过滤掉 docs/spec.md")
|
||||
}
|
||||
if !strings.Contains(filtered, "internal/service/user.go") {
|
||||
t.Errorf("FilterDiff 应当保留正常的业务代码 internal/service/user.go")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user