feat: 添加 Token 费用估算与自定义忽略规则
- 在 LLM 客户端中捕获流式响应的 Token 使用量 - 内置多模型官方定价表,支持自定义输入/输出单价覆盖 - 新增 `--price-input` 和 `--price-output` 配置项 - 支持通过 `.airignore` 文件自定义排除评审文件 - 更新中英日文文档,补充费用估算与忽略规则说明
This commit is contained in:
1 parent
02e2e4edd3
commit
28c29e69df
13 files changed
+691
-82
No files matched your search
+21
-6
@@ -5,18 +5,20 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// AppConfig 全局配置结构体
|
||||
type AppConfig struct {
|
||||
Provider string `yaml:"provider"` // gemini, deepseek, ollama, openai, custom
|
||||
APIKey string `yaml:"api_key"` // API Key
|
||||
BaseURL string `yaml:"base_url"` // 自定义请求地址
|
||||
Model string `yaml:"model"` // 模型名称
|
||||
CustomPrompt string `yaml:"custom_prompt"` // 用户自定义的 Base System Prompt
|
||||
Provider string `yaml:"provider"` // gemini, deepseek, ollama, openai, custom
|
||||
APIKey string `yaml:"api_key"` // API Key
|
||||
BaseURL string `yaml:"base_url"` // 自定义请求地址
|
||||
Model string `yaml:"model"` // 模型名称
|
||||
CustomPrompt string `yaml:"custom_prompt"` // 用户自定义的 Base System Prompt
|
||||
PriceInput float64 `yaml:"price_input,omitempty"` // 自定义输入单价 (每 1M tokens 美元,可选)
|
||||
PriceOutput float64 `yaml:"price_output,omitempty"` // 自定义输出单价 (每 1M tokens 美元,可选)
|
||||
}
|
||||
|
||||
func getConfigPath() (string, error) {
|
||||
@@ -126,6 +128,12 @@ var configSetCmd = &cobra.Command{
|
||||
if pr, _ := cmd.Flags().GetString("prompt"); pr != "" {
|
||||
cfg.CustomPrompt = pr
|
||||
}
|
||||
if pi, _ := cmd.Flags().GetFloat64("price-input"); pi > 0 {
|
||||
cfg.PriceInput = pi
|
||||
}
|
||||
if po, _ := cmd.Flags().GetFloat64("price-output"); po > 0 {
|
||||
cfg.PriceOutput = po
|
||||
}
|
||||
if err := SaveConfig(cfg); err != nil {
|
||||
ui.PrintError("保存配置失败: %v", err)
|
||||
return
|
||||
@@ -160,6 +168,11 @@ var configGetCmd = &cobra.Command{
|
||||
hasPrompt = "是 (已自定义)"
|
||||
}
|
||||
fmt.Printf("CustomPrompt: %s\n", hasPrompt)
|
||||
if cfg.PriceInput > 0 || cfg.PriceOutput > 0 {
|
||||
fmt.Printf("CustomPrice: 输入 $%.4f/1M, 输出 $%.4f/1M (已自定义覆盖)\n", cfg.PriceInput, cfg.PriceOutput)
|
||||
} else {
|
||||
fmt.Printf("CustomPrice: 默认 (跟随官方最新价格表)\n")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,6 +182,8 @@ func init() {
|
||||
configSetCmd.Flags().StringP("model", "m", "", "模型名称")
|
||||
configSetCmd.Flags().StringP("base-url", "u", "", "自定义 API 地址")
|
||||
configSetCmd.Flags().String("prompt", "", "自定义 Base System Prompt")
|
||||
configSetCmd.Flags().Float64("price-input", 0, "自定义每 1M 输入 Token 美元价格")
|
||||
configSetCmd.Flags().Float64("price-output", 0, "自定义每 1M 输出 Token 美元价格")
|
||||
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
configCmd.AddCommand(configGetCmd)
|
||||
|
||||
+40
-7
@@ -3,12 +3,13 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/unclesam-ly/git-air/internal/git"
|
||||
"github.com/unclesam-ly/git-air/internal/llm"
|
||||
"github.com/unclesam-ly/git-air/internal/reviewer"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -53,10 +54,12 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// 3. 初始化大模型客户端
|
||||
llmClient, err := llm.NewClient(llm.Config{
|
||||
Provider: cfg.Provider,
|
||||
APIKey: cfg.APIKey,
|
||||
BaseURL: cfg.BaseURL,
|
||||
Model: cfg.Model,
|
||||
Provider: cfg.Provider,
|
||||
APIKey: cfg.APIKey,
|
||||
BaseURL: cfg.BaseURL,
|
||||
Model: cfg.Model,
|
||||
PriceInput: cfg.PriceInput,
|
||||
PriceOutput: cfg.PriceOutput,
|
||||
})
|
||||
if err != nil {
|
||||
ui.PrintError("初始化模型失败: %v (提示: 请先执行 'git air config set --key YOUR_KEY')", err)
|
||||
@@ -76,15 +79,43 @@ var rootCmd = &cobra.Command{
|
||||
// 6. 打印开场横幅并开始流式评审
|
||||
ui.PrintBanner(cfg.Provider, cfg.Model)
|
||||
printer := ui.NewStreamPrinter()
|
||||
_, err = rev.Execute(ctx, diff, true, func(chunk string) {
|
||||
var reviewOutput strings.Builder
|
||||
_, usage, err := rev.Execute(ctx, diff, true, func(chunk string) {
|
||||
reviewOutput.WriteString(chunk)
|
||||
printer.PrintChunk(chunk)
|
||||
})
|
||||
if err != nil {
|
||||
ui.PrintError("\n评审过程中发生错误: %v", err)
|
||||
return
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 7. 打印 Token 消耗统计
|
||||
if usage != nil {
|
||||
ui.PrintUsage(usage.InputTokens, usage.OutputTokens, usage.EstimateCost())
|
||||
}
|
||||
|
||||
ui.PrintFooter()
|
||||
|
||||
// 8. 门禁阻断检测 (Pre-commit 拦截逻辑)
|
||||
outText := reviewOutput.String()
|
||||
hasBlocker := strings.Contains(outText, "[BLOCKER]") || strings.Contains(outText, "[CRITICAL]")
|
||||
hasReject := strings.Contains(outText, "[REJECT]")
|
||||
hasWarning := strings.Contains(outText, "[WARNING]") || strings.Contains(outText, "[WARN]")
|
||||
|
||||
strict, _ := cmd.Flags().GetBool("strict")
|
||||
noBlock, _ := cmd.Flags().GetBool("no-block")
|
||||
|
||||
if !noBlock {
|
||||
if hasBlocker || hasReject {
|
||||
ui.PrintError("[git-air] 审查未通过:检测到 [BLOCKER] 严重缺陷或 [REJECT] 结论,已拦截提交!")
|
||||
ui.PrintError("修复上述问题后再提交;如需临时跳过可执行 'git commit --no-verify' 或加上 '--no-block'")
|
||||
os.Exit(1)
|
||||
} else if strict && hasWarning {
|
||||
ui.PrintError("[git-air] 严格模式(--strict)生效:检测到 [WARNING] 警告,已拦截提交!")
|
||||
ui.PrintError("请优化上述警告或去掉 --strict 标志。")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -107,4 +138,6 @@ func init() {
|
||||
rootCmd.Flags().String("prompt", "", "临时指定 System Prompt")
|
||||
rootCmd.Flags().Bool("cached", false, "显式审查暂存区(git add)代码")
|
||||
rootCmd.Flags().Bool("staged", false, "显式审查暂存区(git add)代码")
|
||||
rootCmd.Flags().Bool("strict", false, "严格模式:遇到 WARNING 也会阻断退出")
|
||||
rootCmd.Flags().Bool("no-block", false, "非阻断模式:即使发现 BLOCKER 也不以非零退出码阻断")
|
||||
}
|
||||
Reference in new issue
Block a user