Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b0342934 | ||
|
|
02e2e4edd3 |
No files matched your search
@@ -45,14 +45,44 @@
|
||||
它完全剥离聊天上下文,像一个**坐在你工位旁边、嘴有点损但眼神极毒的资深架构师同事**,只对纯粹的代码变更(`git diff`)进行冷酷挑刺:
|
||||
|
||||
- ⚡ **Git 原生极简体验**:无需改变任何工作习惯,在命令行敲 `git air` 即可秒级唤起。
|
||||
- 🧠 **全模型自由接入**:开箱即用预置 **Google Gemini、Anthropic Claude、xAI Grok、DeepSeek、本地离线 Ollama、OpenAI** 等全系列模型。
|
||||
- 🧠 **全模型自由接入**:开箱即用预置 **Google Gemini 3.x、Anthropic Claude、xAI Grok、DeepSeek、阿里通义千问、智谱 GLM、月之暗面 Kimi、本地 Ollama、OpenAI** 等全系列模型。
|
||||
- 🛡️ **智能降噪过滤**:自动剥离 `go.sum`、`package-lock.json`、`*.pb.go`、`*.gen.go` 等锁文件与自动生成代码,省 Token、防死循环。
|
||||
- 🎯 **严谨架构师准则**:直奔主题,聚焦并发竞态、死锁、SQL 注入、空指针、资源泄露,并直接提供修复参考代码。
|
||||
- 📊 **Token 消耗与费用实时估算**:实时捕获并统计输入/输出 Token 消耗,内置 2026 最新官方定价表精准核算每次审查成本(本地离线模型自动标注免费)。
|
||||
- 📋 **团队规范适配(`.airules`)**:在项目根目录放置 `.airules`,AI 自动加载团队专属架构规范并最高优先级执行。
|
||||
- 🪝 **一键 Pre-commit 钩子**:一行命令挂载 Git 提交前自动拦截,在代码上库前把好第一道质量关。
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ 终端效果演示
|
||||
|
||||
```text
|
||||
$ git air
|
||||
|
||||
[git-air] 代码评审中... (Engine: gemini / gemini-3.7-flash)
|
||||
─────────────────────────────────────────────────────────────────
|
||||
#### 变更概述
|
||||
在用户鉴权模块中引入了 Redis 缓存逻辑,并调整了上下文超时传递机制。
|
||||
|
||||
#### 详细审查意见
|
||||
- [BLOCKER] internal/service/user.go:45 - 严重安全隐患: 循环体内直接拼接原生 SQL 字符串,存在 SQL 注入风险。
|
||||
// 建议修复方案:
|
||||
db.Where("username = ?", inputName).First(&user)
|
||||
|
||||
- [WARNING] internal/service/user.go:82 - 潜在风险: 静默丢弃了 Redis 连接异常,当缓存宕机时可能造成缓存击穿。
|
||||
|
||||
- [WARNING] internal/service/user.go:103 - 并发隐患: 提前 return 时漏释放互斥锁(Mutex Leak),高并发下极易引发死锁。
|
||||
|
||||
#### 评审结论
|
||||
- 结论: [REJECT]
|
||||
- 评分: 60 / 100
|
||||
|
||||
📊 Token: 输入 1,243 / 输出 412 | ≈ $0.0025
|
||||
─────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 安装方式
|
||||
|
||||
### 方式 1:通过 Go 一键安装(推荐)
|
||||
@@ -112,9 +142,12 @@ git air config set --provider ollama --model qwen2.5-coder
|
||||
|
||||
# 12. OpenAI 官方 (GPT-4o / o3-mini)
|
||||
git air config set --provider openai --key "YOUR_KEY" --model gpt-4o-mini
|
||||
|
||||
# 13. 自定义 Token 计费费率 (可选:覆盖内置价格表,单位: 美元/1M Tokens)
|
||||
git air config set --price-input 0.75 --price-output 3.75
|
||||
```
|
||||
|
||||
查看当前配置状态:
|
||||
查看当前配置状态(含自定义费率与脱敏 Key):
|
||||
```bash
|
||||
git air config get
|
||||
```
|
||||
@@ -148,7 +181,9 @@ git air --provider deepseek --model deepseek-chat
|
||||
```bash
|
||||
git air hook install
|
||||
```
|
||||
以后团队成员在执行 `git commit` 时,`git-air` 会在终端自动执行审查,若存在阻断性缺陷将自动提醒!
|
||||
- **自动门禁阻断**:当审查发现 `[BLOCKER]` 严重缺陷或 `[REJECT]` 评审结论时,`git-air` 会自动以非零退出码**直接拦截并阻断本次 `git commit`**,死守代码质量底线!
|
||||
- **严格模式 (`--strict`)**:若希望发现 `[WARNING]` 也强制阻断提交,可使用 `git air --strict`;
|
||||
- **临时跳过阻断**:如确需强行提交,可使用 Git 原生命令 `git commit --no-verify` 或附加 `git-air --no-block`。
|
||||
|
||||
如需卸载钩子:
|
||||
```bash
|
||||
@@ -160,7 +195,7 @@ git air hook uninstall
|
||||
## ⚙️ 配置文件说明
|
||||
|
||||
`git-air` 支持多层配置覆盖,优先级从高到低为:
|
||||
1. **命令行 Flags**(如 `--key`, `--model`, `--provider`)
|
||||
1. **命令行 Flags**(如 `--key`, `--model`, `--provider`, `--strict`, `--no-block`)
|
||||
2. **环境变量**(`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
||||
3. **项目级配置**(当前 Git 项目根目录下的 `config.yaml` 或 `.git-air.yaml`)
|
||||
4. **全局用户配置**(`~/.git-air/config.yaml`)
|
||||
@@ -181,6 +216,10 @@ model: "gemini-3.7-flash"
|
||||
|
||||
# 自定义基础提示词 (可选,留空则使用默认资深架构师准则)
|
||||
# custom_prompt: ""
|
||||
|
||||
# 自定义模型单价 (可选,单位: 美元/1M Tokens,用于精准核算自定义中转或企业私有接口费用)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
```
|
||||
|
||||
---
|
||||
@@ -199,6 +238,27 @@ model: "gemini-3.7-flash"
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 自定义忽略审查(`.airignore`)
|
||||
|
||||
除了内置自动忽略的锁文件与二进制外,你还可以在仓库根目录创建 `.airignore` 文件,自定义排除不需要 AI 评审的文件或目录:
|
||||
|
||||
```gitignore
|
||||
# 忽略所有文档与原型设计
|
||||
docs/
|
||||
*.md
|
||||
|
||||
# 忽略测试 Mock 与假数据
|
||||
tests/mock/
|
||||
testdata/
|
||||
|
||||
# 忽略私密配置文件
|
||||
*.env
|
||||
secrets.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 开源许可证
|
||||
|
||||
本项目采用 [MIT 许可证](LICENSE)。欢迎提交 Issue 与 Pull Request!
|
||||
|
||||
+78
-4
@@ -44,14 +44,44 @@ While there are great cloud/CI-based PR review bots, **`git-air` is designed to
|
||||
Completely isolated from your chat history, it acts like a **grumpy but sharp senior architect sitting beside your desk**, ruthlessly inspecting only raw code changes (`git diff`):
|
||||
|
||||
- ⚡ **Git-Native Simplicity**: No workflow changes needed. Just run `git air` in your terminal for instant feedback.
|
||||
- 🧠 **Universal Multi-Model Support**: Pre-configured support for **Google Gemini, Anthropic Claude, xAI Grok, DeepSeek, Local Ollama, OpenAI**, and any OpenAI-compatible custom endpoint.
|
||||
- 🧠 **Universal Multi-Model Support**: Pre-configured support for **Google Gemini 3.x, Anthropic Claude, xAI Grok, DeepSeek, Qwen, Zhipu GLM, Moonshot Kimi, Local Ollama, OpenAI**, and any OpenAI-compatible endpoint.
|
||||
- 🛡️ **Smart Noise Reduction**: Automatically strips out `go.sum`, `package-lock.json`, `*.pb.go`, and lockfiles to save tokens and eliminate review loops.
|
||||
- 🎯 **Strict Senior Architect Persona**: Zero corporate fluff. Directly flags concurrency deadlocks, SQL injections, nil dereferences, and resource leaks with copy-paste code patches.
|
||||
- 📊 **Real-time Token & Cost Estimation**: Accurately tracks prompt/completion token consumption with built-in official pricing rates (local offline models automatically marked free).
|
||||
- 📋 **Team Rules Support (`.airules`)**: Drop an `.airules` file in your repo root, and the AI will prioritize your team-specific guidelines.
|
||||
- 🪝 **One-Click Pre-commit Hook**: Mounts as a local Git hook in one command to block dangerous bugs before code is committed.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Terminal Preview
|
||||
|
||||
```text
|
||||
$ git air
|
||||
|
||||
[git-air] Reviewing code... (Engine: gemini / gemini-3.7-flash)
|
||||
─────────────────────────────────────────────────────────────────
|
||||
#### Change Summary
|
||||
Added Redis caching layer to user authentication and refined context propagation.
|
||||
|
||||
#### Detailed Findings
|
||||
- [BLOCKER] internal/service/user.go:45 - Critical Vulnerability: Direct SQL string concatenation inside loop. Potential SQL injection.
|
||||
// Suggested Fix:
|
||||
db.Where("username = ?", inputName).First(&user)
|
||||
|
||||
- [WARNING] internal/service/user.go:82 - Potential Risk: Silently discarding Redis error. May lead to cache stampede.
|
||||
|
||||
- [WARNING] internal/service/user.go:103 - Concurrency Issue: Mutex unlock skipped on early return branch. High risk of deadlock.
|
||||
|
||||
#### Verdict
|
||||
- Status: [REJECT]
|
||||
- Score: 60 / 100
|
||||
|
||||
📊 Token: Input 1,243 / Output 412 | ≈ $0.0025
|
||||
─────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Option 1: Via Go Install (Recommended)
|
||||
@@ -111,9 +141,12 @@ git air config set --provider ollama --model qwen2.5-coder
|
||||
|
||||
# 12. OpenAI Official (GPT-4o / o3-mini)
|
||||
git air config set --provider openai --key "YOUR_KEY" --model gpt-4o-mini
|
||||
|
||||
# 13. Custom Token Pricing (Optional: override default rates, USD/1M tokens)
|
||||
git air config set --price-input 0.75 --price-output 3.75
|
||||
```
|
||||
|
||||
Check current configuration:
|
||||
Check current configuration (including masked keys and custom rates):
|
||||
```bash
|
||||
git air config get
|
||||
```
|
||||
@@ -147,7 +180,9 @@ Install the hook inside any Git repository:
|
||||
```bash
|
||||
git air hook install
|
||||
```
|
||||
From now on, whenever you run `git commit`, `git-air` will automatically perform a code review in your terminal and warn you of any blocker issues!
|
||||
- **Automated Commit Gate**: When `git-air` flags any `[BLOCKER]` issue or outputs a `[REJECT]` verdict, it automatically exits with a non-zero status code, **directly intercepting and blocking `git commit`**!
|
||||
- **Strict Mode (`--strict`)**: To block commits even on `[WARNING]` issues, use `git air --strict`;
|
||||
- **Bypass Interception**: If you urgently need to bypass the check, use standard Git `git commit --no-verify` or pass `git air --no-block`.
|
||||
|
||||
To uninstall:
|
||||
```bash
|
||||
@@ -159,11 +194,29 @@ git air hook uninstall
|
||||
## ⚙️ Configuration Hierarchy
|
||||
|
||||
`git-air` searches for configurations in the following priority order:
|
||||
1. **CLI Flags** (`--key`, `--model`, `--provider`, `--prompt`)
|
||||
1. **CLI Flags** (`--key`, `--model`, `--provider`, `--prompt`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **Environment Variables** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
||||
3. **Project Config** (`./config.yaml` or `./.git-air.yaml` in repo root)
|
||||
4. **Global User Config** (`~/.git-air/config.yaml`)
|
||||
|
||||
### `config.yaml` Example Template:
|
||||
```yaml
|
||||
# Provider: gemini, claude, grok, deepseek, qwen, zhipu, moonshot, siliconflow, ollama, openai, custom
|
||||
provider: "gemini"
|
||||
api_key: "YOUR_API_KEY_HERE"
|
||||
model: "gemini-3.7-flash"
|
||||
|
||||
# Custom Base URL (Optional)
|
||||
# base_url: "https://api.deepseek.com/v1"
|
||||
|
||||
# Custom System Prompt (Optional)
|
||||
# custom_prompt: ""
|
||||
|
||||
# Custom Token Pricing (Optional, in USD per 1M tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Custom Team Rules (`.airules`)
|
||||
@@ -180,6 +233,27 @@ Create an `.airules` file in your repository root. `git-air` will automatically
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Custom Ignore Rules (`.airignore`)
|
||||
|
||||
In addition to built-in ignored lockfiles and binaries, create an `.airignore` file in your repository root to exclude specific paths from AI review:
|
||||
|
||||
```gitignore
|
||||
# Ignore documentation & design specs
|
||||
docs/
|
||||
*.md
|
||||
|
||||
# Ignore test mocks and fixtures
|
||||
tests/mock/
|
||||
testdata/
|
||||
|
||||
# Ignore sensitive config files
|
||||
*.env
|
||||
secrets.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE). Contributions, PRs, and issues are welcome!
|
||||
|
||||
+77
-4
@@ -44,14 +44,44 @@ GitHubなどのCI/CD上で動作するボットも優秀ですが、**`git-air`
|
||||
チャット履歴のコンテキストを一切排除し、まるで**あなたの隣に座る口は悪いが目の鋭いシニアアーキテクト**のように、純粋な差分(`git diff`)だけを厳しくチェックします:
|
||||
|
||||
- ⚡ **Gitネイティブな極上の操作性**:いつもの作業フローを崩さず、ターミナルで `git air` と叩くだけで即座に起動。
|
||||
- 🧠 **主要モデルを完全網羅**:**Google Gemini、Anthropic Claude、xAI Grok、DeepSeek、ローカルOllama、OpenAI** などを設定なしで即座に切り替え可能。
|
||||
- 🧠 **主要モデルを完全網羅**:**Google Gemini 3.x、Anthropic Claude、xAI Grok、DeepSeek、Qwen、智譜 GLM、Kimi、ローカルOllama、OpenAI** などを設定なしで即座に切り替え可能。
|
||||
- 🛡️ **スマートなノイズ除去**:`go.sum` や `package-lock.json`、自動生成された `*.pb.go` を自動除外。Token消費を抑え、思考ループを防ぎます。
|
||||
- 🎯 **厳格なシニアエンジニア基準**:お世辞や無駄口は一切なし。並行処理の競合、デッドロック、SQLインジェクション、NULLポインタ、リソースリークを指摘し、具体的な修正コードを直接提示します。
|
||||
- 📊 **Token消費量と費用のリアルタイム見積もり**:入出力Token数を正確に集計し、各社最新の公式料金表に基づいて実行コストを即座に計算(ローカル完全オフラインモデルは無料表示)。
|
||||
- 📋 **チーム規約の読み込み(`.airules`)**:リポジトリ直下に `.airules` を配置するだけで、チーム固有のアーキテクチャ規約を最優先で適用。
|
||||
- 🪝 **ワンクリックで Pre-commit フック化**:コマンド1発でGitフックとして登録。危険なコードのコミットを未然にブロックします。
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ ターミナル実行イメージ
|
||||
|
||||
```text
|
||||
$ git air
|
||||
|
||||
[git-air] コードレビュー中... (Engine: gemini / gemini-3.7-flash)
|
||||
─────────────────────────────────────────────────────────────────
|
||||
#### 変更概要
|
||||
ユーザー認証ハンドラーに Redis キャッシュ層を追加し、Context のタイムアウト伝播を修正。
|
||||
|
||||
#### 詳細レビュー結果
|
||||
- [BLOCKER] internal/service/user.go:45 - 重大なセキュリティ脆弱性: ループ内で生SQL文字列を直接結合しています。SQLインジェクションのリスクがあります。
|
||||
// 推奨修正コード:
|
||||
db.Where("username = ?", inputName).First(&user)
|
||||
|
||||
- [WARNING] internal/service/user.go:82 - 潜在的リスク: Redis 接続エラーを握りつぶしています。キャッシュ障害時にDBへ負荷が集中します。
|
||||
|
||||
- [WARNING] internal/service/user.go:103 - 並行処理の欠陥: 早期リターン時に Mutex の Unlock を忘れています(Mutex Leak)。デッドロックの危険性があります。
|
||||
|
||||
#### 結論
|
||||
- 判定: [REJECT]
|
||||
- スコア: 60 / 100
|
||||
|
||||
📊 Token: 入力 1,243 / 出力 412 | ≈ $0.0025
|
||||
─────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 インストール
|
||||
|
||||
### 方法 1: Go Install(推奨)
|
||||
@@ -111,9 +141,12 @@ git air config set --provider ollama --model qwen2.5-coder
|
||||
|
||||
# 12. OpenAI 公式 (GPT-4o / o3-mini)
|
||||
git air config set --provider openai --key "YOUR_KEY" --model gpt-4o-mini
|
||||
|
||||
# 13. カスタム Token 料金設定 (任意:組み込み料金表を上書き、単位: 米ドル/1M Tokens)
|
||||
git air config set --price-input 0.75 --price-output 3.75
|
||||
```
|
||||
|
||||
現在の設定を確認:
|
||||
現在の設定を確認(カスタム料金やマスク化Keyを含む):
|
||||
```bash
|
||||
git air config get
|
||||
```
|
||||
@@ -147,7 +180,9 @@ Git リポジトリ直下で以下を実行:
|
||||
```bash
|
||||
git air hook install
|
||||
```
|
||||
以降、`git commit` を実行するたびにターミナル上で自動レビューが走り、重大な欠陥があればコミット前に警告を出してくれます!
|
||||
- **コミット自動ブロック機能**:レビューで `[BLOCKER]` 重大欠陥や `[REJECT]` 判定が検出された場合、`git-air` は非ゼロの終了コードで**コミット(`git commit`)を自動的に中断・ブロック**します!
|
||||
- **厳格モード (`--strict`)**:`[WARNING]` 警告でもコミットをブロックしたい場合は `git air --strict` を使用;
|
||||
- **一時的なブロック解除**:緊急でコミットを強制したい場合は、Git 標準の `git commit --no-verify` または `git air --no-block` を付与します。
|
||||
|
||||
フックを解除する場合:
|
||||
```bash
|
||||
@@ -158,11 +193,29 @@ git air hook uninstall
|
||||
|
||||
## ⚙️ 設定の優先順位
|
||||
|
||||
1. **コマンドライン引数** (`--key`, `--model`, `--provider`, `--prompt`)
|
||||
1. **コマンドライン引数** (`--key`, `--model`, `--provider`, `--prompt`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **環境変数** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
||||
3. **プロジェクト個別設定** (リポジトリ直下の `./config.yaml` または `./.git-air.yaml`)
|
||||
4. **グローバル設定** (`~/.git-air/config.yaml`)
|
||||
|
||||
### `config.yaml` 設定ファイル例:
|
||||
```yaml
|
||||
# プロバイダー: gemini, claude, grok, deepseek, qwen, zhipu, moonshot, siliconflow, ollama, openai, custom
|
||||
provider: "gemini"
|
||||
api_key: "YOUR_API_KEY_HERE"
|
||||
model: "gemini-3.7-flash"
|
||||
|
||||
# カスタム API エンドポイント (任意)
|
||||
# base_url: "https://api.deepseek.com/v1"
|
||||
|
||||
# カスタム System Prompt (任意)
|
||||
# custom_prompt: ""
|
||||
|
||||
# カスタム Token 料金設定 (任意、単位: 米ドル / 1M Tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 チーム規約のカスタマイズ(`.airules`)
|
||||
@@ -179,6 +232,26 @@ git air hook uninstall
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ カスタム除外設定(`.airignore`)
|
||||
|
||||
組み込みの自動除外(ロックファイルやバイナリ)に加え、リポジトリ直下に `.airignore` を配置することで、任意のファイルやディレクトリをAIレビューから除外できます:
|
||||
|
||||
```gitignore
|
||||
# ドキュメントや設計書を除外
|
||||
docs/
|
||||
*.md
|
||||
|
||||
# テストモックやダミーデータを除外
|
||||
tests/mock/
|
||||
testdata/
|
||||
|
||||
# 機密設定ファイルを除外
|
||||
*.env
|
||||
secrets.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 ライセンス
|
||||
|
||||
本プロジェクトは [MIT ライセンス](LICENSE) のもとで公開されています。Issue や Pull Request も大歓迎です!
|
||||
+16
-1
@@ -2,11 +2,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git-air/internal/ui"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -17,6 +17,8 @@ type AppConfig struct {
|
||||
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)
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"git-air/internal/git"
|
||||
"git-air/internal/ui"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/unclesam-ly/git-air/internal/git"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
+39
-6
@@ -2,13 +2,14 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git-air/internal/git"
|
||||
"git-air/internal/llm"
|
||||
"git-air/internal/reviewer"
|
||||
"git-air/internal/ui"
|
||||
"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"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -57,6 +58,8 @@ var rootCmd = &cobra.Command{
|
||||
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 也不以非零退出码阻断")
|
||||
}
|
||||
@@ -31,3 +31,10 @@ model: "gemini-3.7-flash"
|
||||
|
||||
# 自定义全局评审提示词 (可选,留空则使用内置资深架构师准则)
|
||||
custom_prompt: ""
|
||||
|
||||
# 自定义模型 Token 计费费率 (可选,单位:美元 / 每 1M Tokens)
|
||||
# 默认情况下 git-air 内置了各主流模型官方最新价格表;
|
||||
# 大模型厂商价格可能动态调整,请以实际价格为准
|
||||
# 若你的中转平台价格有变或使用私有计费,可在此自定义覆盖:
|
||||
# price_input: 0.75 # 每 1M 输入 Token 美元单价
|
||||
# price_output: 3.75 # 每 1M 输出 Token 美元单价
|
||||
@@ -1,4 +1,4 @@
|
||||
module git-air
|
||||
module github.com/unclesam-ly/git-air
|
||||
|
||||
go 1.26.6
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+74
-16
@@ -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)
|
||||
baseName := filepath.Base(cleanPath)
|
||||
// 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()
|
||||
|
||||
for _, pattern := range ignoredPatterns {
|
||||
// 精确匹配文件名(如 go.sum)
|
||||
if pattern == baseName {
|
||||
return true
|
||||
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
|
||||
}
|
||||
|
||||
// 通配符匹配(如 *.pb.go)
|
||||
if matched, _ := filepath.Match(pattern, baseName); matched {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+142
-6
@@ -16,12 +16,16 @@ type Config struct {
|
||||
APIKey string // 密钥 (Ollama 本地可填任意字符或留空)
|
||||
BaseURL string // 自定义接口地址 (非必填,会自动根据 Provider 设置默认值)
|
||||
Model string // 模型名称 (非必填,会自动设置默认主力模型)
|
||||
PriceInput float64 // 用户自定义每 1M 输入 Token 美元价格 (可选,覆盖内置价格表)
|
||||
PriceOutput float64 // 用户自定义每 1M 输出 Token 美元价格 (可选,覆盖内置价格表)
|
||||
}
|
||||
|
||||
// Client 统一大模型客户端
|
||||
type Client struct {
|
||||
client *openai.Client
|
||||
model string
|
||||
priceInput float64
|
||||
priceOutput float64
|
||||
}
|
||||
|
||||
// NewClient 初始化客户端
|
||||
@@ -229,6 +233,8 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
return &Client{
|
||||
client: openai.NewClientWithConfig(clientConfig),
|
||||
model: model,
|
||||
priceInput: cfg.PriceInput,
|
||||
priceOutput: cfg.PriceOutput,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -254,8 +260,122 @@ func (c *Client) Review(ctx context.Context, systemPrompt, diff string) (string,
|
||||
return resp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
// ReviewStream 流式逐字返回评审结果(打字机效果)
|
||||
func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, onChunk func(chunk string)) error {
|
||||
// Usage 记录一次调用的 Token 消耗
|
||||
type Usage struct {
|
||||
InputTokens int
|
||||
OutputTokens int
|
||||
Model string
|
||||
Provider string
|
||||
CustomPriceInput float64 // 用户自定义输入单价(每 1M tokens 美元,>0 优先使用)
|
||||
CustomPriceOutput float64 // 用户自定义输出单价(每 1M tokens 美元,>0 优先使用)
|
||||
}
|
||||
|
||||
// reviewPriceTable 各模型每 1M token 的美元单价(输入/输出)
|
||||
// 数据来源:各厂商官方定价页面,更新于 2026-08
|
||||
// 如实际价格有变动,可在 config.yaml 中通过 price_input/price_output 字段自定义覆盖
|
||||
var reviewPriceTable = map[string][2]float64{
|
||||
|
||||
// ── Google Gemini ─────────────────────────────────────────────
|
||||
// 3.x 系列(当前主力)
|
||||
"gemini-3.7-flash": {0.75, 3.75}, // 推介价至 2026-12-31,之后 1.50/7.50
|
||||
"gemini-3.6-flash": {1.50, 7.50},
|
||||
"gemini-3.5-flash": {1.50, 9.00},
|
||||
"gemini-3.5-flash-lite": {0.30, 2.50},
|
||||
"gemini-3.1-flash-lite": {0.25, 1.50},
|
||||
"gemini-3.1-pro": {2.00, 12.00},
|
||||
// 2.5 系列(2026-10 退役)
|
||||
"gemini-2.5-pro": {1.25, 10.00},
|
||||
"gemini-2.5-flash": {0.30, 2.50},
|
||||
"gemini-2.5-flash-lite": {0.10, 0.40},
|
||||
// 旧款(兼容)
|
||||
"gemini-2.0-flash": {0.10, 0.40},
|
||||
|
||||
// ── DeepSeek ─────────────────────────────────────────────────
|
||||
// 注意:DeepSeek 已启用峰谷定价,以下为非高峰标准价
|
||||
"deepseek-v4-flash": {0.22, 0.66},
|
||||
"deepseek-chat": {0.22, 0.66}, // V4-Flash 别名
|
||||
"deepseek-reasoner": {0.55, 2.19},
|
||||
|
||||
// ── Anthropic Claude ──────────────────────────────────────────
|
||||
"claude-opus-5": {5.00, 25.00},
|
||||
"claude-sonnet-5": {2.00, 10.00}, // 推介价至 2026-08-31
|
||||
"anthropic/claude-sonnet-5": {2.00, 10.00},
|
||||
"claude-haiku-4-5": {1.00, 5.00},
|
||||
"anthropic/claude-haiku-4-5": {1.00, 5.00},
|
||||
// 旧款(兼容)
|
||||
"anthropic/claude-3.7-sonnet": {3.00, 15.00},
|
||||
"anthropic/claude-3.5-sonnet": {3.00, 15.00},
|
||||
"anthropic/claude-3.5-haiku": {0.80, 4.00},
|
||||
|
||||
// ── xAI Grok ─────────────────────────────────────────────────
|
||||
"grok-4.6": {2.00, 6.00}, // >200k tokens 翻倍
|
||||
"grok-4.3": {1.25, 2.50},
|
||||
"grok-2-latest": {2.00, 6.00}, // 旧款映射
|
||||
|
||||
// ── OpenAI ───────────────────────────────────────────────────
|
||||
"gpt-5.6-sol": {5.00, 30.00},
|
||||
"gpt-5.6-terra": {2.00, 12.00},
|
||||
"gpt-5.6-luna": {0.20, 1.20},
|
||||
"gpt-4o": {2.50, 10.00}, // 旧款保留
|
||||
"gpt-4o-mini": {0.15, 0.60},
|
||||
"o3-mini": {1.10, 4.40},
|
||||
|
||||
// ── SiliconFlow(聚合平台,按 DeepSeek V4 主力估算)────────────
|
||||
"deepseek-ai/DeepSeek-V3": {0.22, 0.66},
|
||||
"deepseek-ai/DeepSeek-R1": {0.55, 2.19},
|
||||
|
||||
// ── Qwen / 阿里通义千问 ───────────────────────────────────────
|
||||
"qwen-3.8-max": {2.00, 6.00},
|
||||
"qwen-max": {2.00, 6.00},
|
||||
"qwen-plus": {0.40, 1.20},
|
||||
"qwen-3.7-flash": {0.03, 0.13},
|
||||
|
||||
// ── Kimi / Moonshot AI ────────────────────────────────────────
|
||||
"kimi-k3": {3.00, 15.00},
|
||||
"moonshot-v1-8k": {0.95, 4.00},
|
||||
"moonshot-v1-32k": {1.90, 8.00},
|
||||
"moonshot-v1-128k": {3.50, 15.00},
|
||||
|
||||
// ── 智谱 GLM ─────────────────────────────────────────────────
|
||||
"glm-5.3": {1.40, 4.40},
|
||||
"glm-5.2": {1.40, 4.40},
|
||||
"glm-4-plus": {1.40, 4.40},
|
||||
|
||||
// ── Doubao / 字节跳动 ─────────────────────────────────────────
|
||||
"doubao-seed-2.1-pro": {0.88, 4.42},
|
||||
"doubao-pro-32k": {0.88, 4.42},
|
||||
|
||||
// ── Groq(超快推理,按量计费)────────────────────────────────
|
||||
"llama-3.3-70b-versatile": {0.59, 0.79},
|
||||
"llama-3.1-8b-instant": {0.05, 0.08},
|
||||
|
||||
// ── 本地模型(完全免费)──────────────────────────────────────
|
||||
"ollama": {0, 0},
|
||||
"local-model": {0, 0},
|
||||
}
|
||||
|
||||
// EstimateCost 估算费用(美元),返回 -1 表示无定价数据
|
||||
func (u *Usage) EstimateCost() float64 {
|
||||
// 1. 优先使用用户在 config.yaml 中配置的自定义单价(每 1M tokens 美元)
|
||||
if u.CustomPriceInput > 0 || u.CustomPriceOutput > 0 {
|
||||
inputCost := float64(u.InputTokens) / 1_000_000 * u.CustomPriceInput
|
||||
outputCost := float64(u.OutputTokens) / 1_000_000 * u.CustomPriceOutput
|
||||
return inputCost + outputCost
|
||||
}
|
||||
|
||||
// 2. 回退使用内置官方模型价格表
|
||||
prices, ok := reviewPriceTable[u.Model]
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
// Ollama / 本地模型 price = 0,直接返回 0
|
||||
inputCost := float64(u.InputTokens) / 1_000_000 * prices[0]
|
||||
outputCost := float64(u.OutputTokens) / 1_000_000 * prices[1]
|
||||
return inputCost + outputCost
|
||||
}
|
||||
|
||||
// ReviewStream 流式逐字返回评审结果(打字机效果),同时返回 Token 使用量
|
||||
func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, onChunk func(chunk string)) (*Usage, error) {
|
||||
stream, err := c.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
|
||||
Model: c.model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
@@ -264,18 +384,27 @@ func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, on
|
||||
},
|
||||
Temperature: 0.2,
|
||||
Stream: true,
|
||||
StreamOptions: &openai.StreamOptions{
|
||||
IncludeUsage: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建流式请求失败: %w", err)
|
||||
return nil, fmt.Errorf("创建流式请求失败: %w", err)
|
||||
}
|
||||
if stream != nil {
|
||||
defer stream.Close()
|
||||
}
|
||||
|
||||
usage := &Usage{
|
||||
Model: c.model,
|
||||
CustomPriceInput: c.priceInput,
|
||||
CustomPriceOutput: c.priceOutput,
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -284,8 +413,15 @@ func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, on
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("流式读取异常: %w", err)
|
||||
return nil, fmt.Errorf("流式读取异常: %w", err)
|
||||
}
|
||||
|
||||
// 捕获最后一个 chunk 中携带的 Usage 信息
|
||||
if response.Usage.TotalTokens > 0 {
|
||||
usage.InputTokens = response.Usage.PromptTokens
|
||||
usage.OutputTokens = response.Usage.CompletionTokens
|
||||
}
|
||||
|
||||
if len(response.Choices) > 0 {
|
||||
chunk := response.Choices[0].Delta.Content
|
||||
if chunk != "" {
|
||||
@@ -294,5 +430,5 @@ func (c *Client) ReviewStream(ctx context.Context, systemPrompt, diff string, on
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return usage, nil
|
||||
}
|
||||
@@ -3,8 +3,9 @@ package reviewer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git-air/internal/llm"
|
||||
"strings"
|
||||
|
||||
"github.com/unclesam-ly/git-air/internal/llm"
|
||||
)
|
||||
|
||||
type Reviewer struct {
|
||||
@@ -23,18 +24,19 @@ func NewReviewer(client *llm.Client, repoRoot string, customPrompt string) *Revi
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行代码评审
|
||||
func (r *Reviewer) Execute(ctx context.Context, diff string, stream bool, onChunk func(chunk string)) (string, error) {
|
||||
// Execute 执行代码评审,返回 (result, usage, error)
|
||||
func (r *Reviewer) Execute(ctx context.Context, diff string, stream bool, onChunk func(chunk string)) (string, *llm.Usage, error) {
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
return "", fmt.Errorf("diff 内容为空,无需评审")
|
||||
return "", nil, 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
|
||||
usage, err := r.client.ReviewStream(ctx, systemPrompt, diff, onChunk)
|
||||
return "", usage, err
|
||||
}
|
||||
|
||||
return r.client.Review(ctx, systemPrompt, diff)
|
||||
result, err := r.client.Review(ctx, systemPrompt, diff)
|
||||
return result, nil, err
|
||||
}
|
||||
@@ -30,6 +30,28 @@ func PrintFooter() {
|
||||
fmt.Println(colorGray + strings.Repeat("─", 65) + colorReset)
|
||||
}
|
||||
|
||||
// PrintUsage 打印 Token 消耗与费用估算
|
||||
func PrintUsage(inputTokens, outputTokens int, costUSD float64) {
|
||||
if inputTokens == 0 && outputTokens == 0 {
|
||||
return
|
||||
}
|
||||
costStr := ""
|
||||
if costUSD < 0 {
|
||||
costStr = "费用未知"
|
||||
} else if costUSD == 0 {
|
||||
costStr = "本地免费"
|
||||
} else {
|
||||
costStr = fmt.Sprintf("≈ $%.4f", costUSD)
|
||||
}
|
||||
|
||||
fmt.Printf("\n- %sToken: 输入 %s%d%s / 输出 %s%d%s | %s%s%s\n",
|
||||
colorBold,
|
||||
colorCyan, inputTokens, colorGray,
|
||||
colorCyan, outputTokens, colorGray,
|
||||
colorCyan, costStr, colorReset,
|
||||
)
|
||||
}
|
||||
|
||||
// PrintError 打印错误信息
|
||||
func PrintError(format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
|
||||
Reference in new issue
Block a user