- 在 LLM 客户端中捕获流式响应的 Token 使用量 - 内置多模型官方定价表,支持自定义输入/输出单价覆盖 - 新增 `--price-input` 和 `--price-output` 配置项 - 支持通过 `.airignore` 文件自定义排除评审文件 - 更新中英日文文档,补充费用估算与忽略规则说明
156 lines
3.6 KiB
Go
156 lines
3.6 KiB
Go
package git
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// 默认需要忽略的文件名与后缀列表
|
|
var defaultIgnoredPatterns = []string{
|
|
// 依赖与锁文件
|
|
"go.sum",
|
|
"package-lock.json",
|
|
"yarn.lock",
|
|
"pnpm-lock.yaml",
|
|
"composer.lock",
|
|
"Cargo.lock",
|
|
"Gemfile.lock",
|
|
// 自动生成的代码
|
|
"*.pb.go",
|
|
"*.gen.go",
|
|
"*_generated.go",
|
|
"*.g.dart",
|
|
// 压缩/静态构建产物
|
|
"*.min.js",
|
|
"*.min.css",
|
|
"*.map",
|
|
"*.bundle.js",
|
|
// 图片与多媒体
|
|
"*.png",
|
|
"*.jpg",
|
|
"*.jpeg",
|
|
"*.gif",
|
|
"*.svg",
|
|
"*.ico",
|
|
"*.pdf",
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 1. 先匹配用户自定义规则 (.airignore)
|
|
for _, pattern := range customPatterns {
|
|
if matchPattern(pattern, cleanPath, baseName) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// 2. 再匹配内置默认规则
|
|
for _, pattern := range defaultIgnoredPatterns {
|
|
if matchPattern(pattern, cleanPath, baseName) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// matchPattern 单条规则智能匹配(支持目录/、通配符*、精确匹配)
|
|
func matchPattern(pattern, cleanPath, baseName string) bool {
|
|
pattern = filepath.ToSlash(strings.TrimSpace(pattern))
|
|
if pattern == "" {
|
|
return false
|
|
}
|
|
|
|
// 目录规则:如 "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 ""
|
|
}
|
|
|
|
// 按照 diff --git 切分各个文件的差异区块
|
|
chunks := strings.Split(rawDiff, "diff --git ")
|
|
var filteredChunks []string
|
|
|
|
for _, chunk := range chunks {
|
|
if strings.TrimSpace(chunk) == "" {
|
|
continue
|
|
}
|
|
|
|
// 还原切分前缀
|
|
fullChunk := "diff --git " + chunk
|
|
lines := strings.Split(chunk, "\n")
|
|
if len(lines) == 0 {
|
|
continue
|
|
}
|
|
|
|
// 解析第一行中的文件路径,如:a/internal/ent/schema.go b/internal/ent/schema.go
|
|
firstLine := lines[0]
|
|
parts := strings.Fields(firstLine)
|
|
if len(parts) >= 2 {
|
|
filePath := strings.TrimPrefix(parts[1], "b/")
|
|
if isIgnoredFile(filePath, customPatterns) {
|
|
continue // 命中忽略规则,跳过这个文件的 diff
|
|
}
|
|
}
|
|
|
|
filteredChunks = append(filteredChunks, fullChunk)
|
|
}
|
|
|
|
return strings.Join(filteredChunks, "")
|
|
}
|