feat: 拆分评审与提交消息提示词并增加 hook 备份与 JSON 输出

- 新增 custom_reviewer_prompt 和 custom_commit_msg_prompt,兼容旧 custom_prompt
- pre-commit hook 安装时备份已有 hook,卸载时恢复
- 支持 --format json 输出评审结果
- diff 发送前自动脱敏敏感信息
This commit is contained in:
unclesam committed 2026-08-23 22:31:30 +08:00
1 parent 39c58316a5
commit b04d70b99a
18 files changed
+637 -45

No files matched your search

+11
View File
@@ -95,3 +95,14 @@ func BuildCommitMsgPrompt(lang string) string {
%s
`, langName, exampleSummary, exampleBullet1, exampleBullet2)
}
// BuildCommitMsgPromptWithCustom 在内置 Conventional Commits 约束基础上追加用户规则。
// 保留内置约束,避免自定义提示词意外要求模型输出非法提交格式。
func BuildCommitMsgPromptWithCustom(lang, customPrompt string) string {
basePrompt := BuildCommitMsgPrompt(lang)
customPrompt = strings.TrimSpace(customPrompt)
if customPrompt == "" {
return basePrompt
}
return customPrompt + "\n\n### git-air 内置提交信息约束(必须遵守):\n" + basePrompt
}
+10
View File
@@ -56,3 +56,13 @@ func TestBuildCommitMsgPrompt(t *testing.T) {
}
}
}
func TestBuildCommitMsgPromptWithCustom(t *testing.T) {
prompt := BuildCommitMsgPromptWithCustom("en", "Use a short imperative subject and mention breaking changes explicitly.")
if !strings.Contains(prompt, "Use a short imperative subject") {
t.Fatal("自定义 Commit Message Prompt 未被合并")
}
if !strings.Contains(prompt, "Conventional Commits") {
t.Fatal("内置 Conventional Commits 约束不应丢失")
}
}
+73
View File
@@ -0,0 +1,73 @@
package reviewer
import (
"regexp"
"strconv"
"strings"
)
type Finding struct {
Severity string `json:"severity"`
Location string `json:"location,omitempty"`
Message string `json:"message"`
}
type ReviewReport struct {
Format string `json:"format"`
Verdict string `json:"verdict,omitempty"`
Score int `json:"score,omitempty"`
Findings []Finding `json:"findings"`
Raw string `json:"raw"`
}
var (
verdictPattern = regexp.MustCompile(`(?mi)(?:结论|verdict)\s*[::]\s*\[?(PASS|WARN|WARNING|REJECT|BLOCKER|CRITICAL)\]?`)
scorePattern = regexp.MustCompile(`(?mi)(?:评分|score)\s*[::]\s*(\d{1,3})`)
findingPattern = regexp.MustCompile(`(?m)^\s*-\s*\[(BLOCKER|CRITICAL|WARNING|WARN|SUGGESTION|INFO)\]\s+(.+?)\s*$`)
)
// ParseReviewReport 将模型的 Markdown Review 转成稳定的 JSON 结构。
// Raw 始终保留完整原文,解析失败时下游仍可自行处理。
func ParseReviewReport(raw string) ReviewReport {
report := ReviewReport{
Format: "git-air-review/v1",
Findings: []Finding{},
Raw: strings.TrimSpace(raw),
}
if match := verdictPattern.FindStringSubmatch(raw); len(match) > 1 {
report.Verdict = strings.ToUpper(match[1])
}
if match := scorePattern.FindStringSubmatch(raw); len(match) > 1 {
report.Score, _ = strconv.Atoi(match[1])
if report.Score > 100 {
report.Score = 100
}
}
for _, match := range findingPattern.FindAllStringSubmatch(raw, -1) {
location, message := splitFindingContent(match[2])
report.Findings = append(report.Findings, Finding{
Severity: strings.ToUpper(match[1]),
Location: location,
Message: message,
})
}
return report
}
func splitFindingContent(content string) (string, string) {
content = strings.TrimSpace(content)
parts := strings.SplitN(content, " - ", 2)
if len(parts) == 1 {
return "", content
}
location := strings.TrimSpace(parts[0])
message := strings.TrimSpace(parts[1])
// 没有行号/路径特征时,不要误把普通句子当成 location。
if !strings.Contains(location, ":") {
return "", content
}
return location, message
}
+25
View File
@@ -0,0 +1,25 @@
package reviewer
import "testing"
func TestParseReviewReport(t *testing.T) {
raw := `#### 详细审查意见
- [BLOCKER] internal/user.go:45 - 存在 SQL 注入风险
- [WARNING] internal/user.go:82 - Redis 错误被忽略
- [WARNING] 缺少超时控制
#### 评审结论
- 结论: [REJECT]
- 评分: 58 / 100
`
report := ParseReviewReport(raw)
if report.Verdict != "REJECT" || report.Score != 58 {
t.Fatalf("结论解析错误: %+v", report)
}
if len(report.Findings) != 3 || report.Findings[0].Location != "internal/user.go:45" {
t.Fatalf("问题解析错误: %+v", report.Findings)
}
if report.Findings[2].Location != "" || report.Findings[2].Message != "缺少超时控制" {
t.Fatalf("无位置信息的问题解析错误: %+v", report.Findings[2])
}
}
+2 -2
View File
@@ -42,13 +42,13 @@ func (r *Reviewer) Execute(ctx context.Context, diff string, stream bool, onChun
}
// GenerateCommitMsg 根据 Diff 智能生成符合 Conventional Commits 规范的提交信息
func (r *Reviewer) GenerateCommitMsg(ctx context.Context, diff string, lang string) (string, error) {
func (r *Reviewer) GenerateCommitMsg(ctx context.Context, diff string, lang string, customPrompt string) (string, error) {
if strings.TrimSpace(diff) == "" {
return "", fmt.Errorf("diff 内容为空,无法生成 Commit Message")
}
targetLang := DetectLanguage(lang)
systemPrompt := BuildCommitMsgPrompt(targetLang)
systemPrompt := BuildCommitMsgPromptWithCustom(targetLang, customPrompt)
msg, err := r.client.Review(ctx, systemPrompt, diff)
if err != nil {