420 lines
9.5 KiB
Markdown
420 lines
9.5 KiB
Markdown
|
|
# 后端服务层
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
后端服务层是 PR-Helper 的核心业务逻辑层,位于 handlers 和数据层之间。主要职责包括 Git 操作、LLM 调用、PR 生成和代码审查。
|
|||
|
|
|
|||
|
|
## 服务架构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
handlers/
|
|||
|
|
↓ 调用
|
|||
|
|
services/
|
|||
|
|
├── git.go # Git 操作
|
|||
|
|
├── llm.go # LLM API 调用
|
|||
|
|
├── generate.go # PR 描述生成
|
|||
|
|
├── review.go # AI 代码审查
|
|||
|
|
├── cache.go # 缓存管理
|
|||
|
|
└── notes.go # 审查笔记
|
|||
|
|
↓ 使用
|
|||
|
|
database/ + filesystem/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Git 服务 (git.go)
|
|||
|
|
|
|||
|
|
### 核心功能
|
|||
|
|
|
|||
|
|
#### 克隆仓库
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func Clone(opts CloneOptions, progressFn func(event string, data interface{})) (*CloneResult, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 支持基本认证(用户名/密码)
|
|||
|
|
- 可选深度克隆
|
|||
|
|
- 进度回调(SSE 事件)
|
|||
|
|
- 返回仓库路径、分支、标签、提交数、大小
|
|||
|
|
|
|||
|
|
#### 获取引用
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GetRefs(repo *git.Repository) ([]RefInfo, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 返回所有分支和标签
|
|||
|
|
- 标记 HEAD 分支
|
|||
|
|
- 解析注解标签到提交
|
|||
|
|
|
|||
|
|
#### 获取图形数据
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GetGraph(repo *git.Repository, maxCommits int) (*GraphData, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- D3.js 兼容格式
|
|||
|
|
- 包含提交、引用、边
|
|||
|
|
- BFS 遍历,限制最大提交数
|
|||
|
|
|
|||
|
|
#### 获取差异
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GetDiff(repo *git.Repository, baseRef, headRef string) (string, error)
|
|||
|
|
func GetDiffFiles(repo *git.Repository, baseRef, headRef string) ([]FileDiff, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 统一差异格式
|
|||
|
|
- 按文件分割
|
|||
|
|
- 解析变更行数
|
|||
|
|
|
|||
|
|
#### 获取提交日志
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GetCommitLog(repo *git.Repository, refName string, maxCommits int) ([]CommitInfo, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- BFS 遍历父提交
|
|||
|
|
- 限制最大提交数
|
|||
|
|
- 包含作者、时间、父提交
|
|||
|
|
|
|||
|
|
### 数据结构
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type RefInfo struct {
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Hash string `json:"hash"`
|
|||
|
|
IsHead bool `json:"is_head"`
|
|||
|
|
IsTag bool `json:"is_tag"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type CommitInfo struct {
|
|||
|
|
Hash string `json:"hash"`
|
|||
|
|
ShortHash string `json:"short_hash"`
|
|||
|
|
Message string `json:"message"`
|
|||
|
|
Author string `json:"author"`
|
|||
|
|
Email string `json:"email"`
|
|||
|
|
Timestamp string `json:"timestamp"`
|
|||
|
|
ParentIDs []string `json:"parent_ids"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type GraphData struct {
|
|||
|
|
Commits []CommitInfo `json:"commits"`
|
|||
|
|
Refs []RefInfo `json:"refs"`
|
|||
|
|
Edges []Edge `json:"edges"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type FileDiff struct {
|
|||
|
|
Filename string `json:"filename"`
|
|||
|
|
Patch string `json:"patch"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## LLM 服务 (llm.go)
|
|||
|
|
|
|||
|
|
### 配置管理
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type LLMConfig struct {
|
|||
|
|
Endpoint string
|
|||
|
|
APIKey string
|
|||
|
|
Model string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func GetLLMConfig(db *sql.DB, userID int64) (LLMConfig, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 从 user_settings 表读取
|
|||
|
|
- 每用户独立配置
|
|||
|
|
- 默认模型: deepseek-v4-pro
|
|||
|
|
|
|||
|
|
### 流式调用
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type StreamCallback func(event string, data interface{})
|
|||
|
|
|
|||
|
|
func ChatStream(config LLMConfig, messages []goopenai.ChatCompletionMessage, callback StreamCallback) (string, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 使用 go-openai 客户端
|
|||
|
|
- SSE 流式响应
|
|||
|
|
- 回调函数处理每个 chunk
|
|||
|
|
- 返回完整响应文本
|
|||
|
|
|
|||
|
|
### JSON 提取
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func extractJSON(s string) string
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 从 Markdown 代码块提取
|
|||
|
|
- 从混合文本中提取 JSON 对象/数组
|
|||
|
|
- 处理嵌套括号
|
|||
|
|
|
|||
|
|
## PR 生成服务 (generate.go)
|
|||
|
|
|
|||
|
|
### 生成流程
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback StreamCallback) (string, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
1. **读取 LLM 配置**: 从用户设置获取
|
|||
|
|
2. **打开仓库**: 使用 go-git
|
|||
|
|
3. **获取提交**: 过滤 base 和 head 之间的提交
|
|||
|
|
4. **获取差异**: 生成统一差异
|
|||
|
|
5. **构建提示**: 包含提交记录和差异
|
|||
|
|
6. **调用 LLM**: 流式生成
|
|||
|
|
7. **返回结果**: Markdown 格式的 PR 描述
|
|||
|
|
|
|||
|
|
### 提示模板
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。
|
|||
|
|
|
|||
|
|
## Commit 记录
|
|||
|
|
{commits}
|
|||
|
|
|
|||
|
|
## 代码变更 (Diff)
|
|||
|
|
{diff}
|
|||
|
|
|
|||
|
|
请直接输出 Markdown 格式的 PR 描述,包含以下部分:
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
**类型**: feat|fix|refactor|docs|chore|style|test|perf
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
一段话概述变更内容
|
|||
|
|
|
|||
|
|
## 详细说明
|
|||
|
|
按模块分组的详细变更说明
|
|||
|
|
|
|||
|
|
## 影响范围
|
|||
|
|
影响范围说明
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 大型差异处理
|
|||
|
|
|
|||
|
|
- 截断到 60k 字符
|
|||
|
|
- 保留完整提交记录
|
|||
|
|
- 标记截断位置
|
|||
|
|
|
|||
|
|
## 代码审查服务 (review.go)
|
|||
|
|
|
|||
|
|
### 审查流程
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GenerateReview(db *sql.DB, repoPath, base, head string, topN, concurrency int, userID int64, callback StreamCallback) (*ReviewResult, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
1. **获取差异文件**: 按文件分割
|
|||
|
|
2. **排序**: 按变更行数降序
|
|||
|
|
3. **Top-N 过滤**: 只分析前 N 个文件
|
|||
|
|
4. **并发审查**: 使用信号量控制并发
|
|||
|
|
5. **生成汇总**: 聚合所有文件结果
|
|||
|
|
6. **返回结果**: 结构化 JSON
|
|||
|
|
|
|||
|
|
### Top-N 策略
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 按变更行数排序
|
|||
|
|
sort.Slice(files, func(i, j int) bool {
|
|||
|
|
return countDiffLines(files[i].Patch) > countDiffLines(files[j].Patch)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 应用 Top-N
|
|||
|
|
if topN > 0 && topN < len(files) {
|
|||
|
|
files = files[:topN]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 默认 Top-N: 20
|
|||
|
|
- 可通过设置或请求参数调整
|
|||
|
|
- 0 表示分析所有文件
|
|||
|
|
|
|||
|
|
### 并发控制
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
sem := make(chan struct{}, concurrency)
|
|||
|
|
var wg sync.WaitGroup
|
|||
|
|
|
|||
|
|
for i, file := range files {
|
|||
|
|
wg.Add(1)
|
|||
|
|
go func(idx int, f FileDiff) {
|
|||
|
|
defer wg.Done()
|
|||
|
|
sem <- struct{}{} // 获取槽位
|
|||
|
|
defer func() { <-sem }() // 释放槽位
|
|||
|
|
// ... 审查逻辑
|
|||
|
|
}(i, file)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 默认并发数: 5
|
|||
|
|
- 信号量控制最大并发
|
|||
|
|
- 线程安全的回调函数
|
|||
|
|
|
|||
|
|
### 单文件审查提示
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
你是一个资深代码审查专家。请审查以下代码变更,给出专业的 Review 意见。
|
|||
|
|
|
|||
|
|
## 文件: {filename}
|
|||
|
|
## 变更行数: +{additions} / -{deletions}
|
|||
|
|
|
|||
|
|
## Diff
|
|||
|
|
{patch}
|
|||
|
|
|
|||
|
|
请按以下 JSON 格式输出审查意见(直接输出 JSON 数组,不要包含 markdown 代码块标记):
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"severity": "critical 或 warning 或 info",
|
|||
|
|
"description": "问题描述",
|
|||
|
|
"suggestion": "建议的修改方案",
|
|||
|
|
"code_example": "建议的代码(如有)"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
严重程度说明:
|
|||
|
|
- critical: 严重问题(安全漏洞、数据丢失风险、崩溃风险)
|
|||
|
|
- warning: 建议改进(性能问题、代码规范、可维护性)
|
|||
|
|
- info: 提示信息(最佳实践、可选优化)
|
|||
|
|
|
|||
|
|
如果代码没有问题,输出空数组 []。
|
|||
|
|
请用中文回复。
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 汇总生成
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
以下是多个文件的代码审查结果,请给出整体评估:
|
|||
|
|
|
|||
|
|
{reviews}
|
|||
|
|
|
|||
|
|
请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
|
|||
|
|
{
|
|||
|
|
"score": 7,
|
|||
|
|
"overall": "总体评价(2-3 句话)",
|
|||
|
|
"findings": "按严重程度排序的主要发现汇总",
|
|||
|
|
"recommendations": "改进建议,用换行分隔多条建议"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 数据结构
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type ReviewSuggestion struct {
|
|||
|
|
Severity string `json:"severity"`
|
|||
|
|
Description string `json:"description"`
|
|||
|
|
Suggestion string `json:"suggestion"`
|
|||
|
|
CodeExample string `json:"code_example,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type FileReview struct {
|
|||
|
|
FileName string `json:"file_name"`
|
|||
|
|
ChangeLines int `json:"change_lines"`
|
|||
|
|
Suggestions []ReviewSuggestion `json:"suggestions"`
|
|||
|
|
RawReview string `json:"raw_review"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type ReviewSummary struct {
|
|||
|
|
Score int `json:"score"`
|
|||
|
|
Overall string `json:"overall"`
|
|||
|
|
Findings string `json:"findings"`
|
|||
|
|
Recommendations string `json:"recommendations"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type ReviewResult struct {
|
|||
|
|
FileReviews []FileReview `json:"file_reviews"`
|
|||
|
|
Summary ReviewSummary `json:"summary"`
|
|||
|
|
TopN int `json:"top_n"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## SSE 事件格式
|
|||
|
|
|
|||
|
|
### PR 生成事件
|
|||
|
|
|
|||
|
|
| 事件 | 数据 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `content` | `{content: string}` | LLM 输出的 Markdown 片段 |
|
|||
|
|
| `done` | `{content: ""}` | 生成完成 |
|
|||
|
|
|
|||
|
|
### 代码审查事件
|
|||
|
|
|
|||
|
|
| 事件 | 数据 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `start` | `{total_files, reviewed_files, top_n}` | 审查开始 |
|
|||
|
|
| `file_start` | `{file, index, total}` | 开始审查文件 |
|
|||
|
|
| `content` | `{content: string}` | LLM 输出片段 |
|
|||
|
|
| `suggestion` | `{file, severity, content}` | 审查建议 |
|
|||
|
|
| `file_end` | `{file}` | 文件审查完成 |
|
|||
|
|
| `summary` | `{score, overall, findings, recommendations}` | 汇总结果 |
|
|||
|
|
| `progress` | `{step: string}` | 进度更新 |
|
|||
|
|
| `error` | `{message: string}` | 错误信息 |
|
|||
|
|
| `done` | `{content: ""}` | 审查完成 |
|
|||
|
|
|
|||
|
|
## 缓存服务 (cache.go)
|
|||
|
|
|
|||
|
|
### 仓库缓存
|
|||
|
|
|
|||
|
|
- 存储路径: `data/repos/`
|
|||
|
|
- 命名规则: `{url}_{timestamp}`
|
|||
|
|
- 过期清理: 可配置天数(默认 7 大)
|
|||
|
|
- 大小限制: 可配置最大大小(默认 5GB)
|
|||
|
|
|
|||
|
|
### 缓存生命周期
|
|||
|
|
|
|||
|
|
1. **克隆**: 首次访问时克隆
|
|||
|
|
2. **使用**: 更新 last_used 时间
|
|||
|
|
3. **过期**: 定期清理过期仓库
|
|||
|
|
4. **删除**: 手动或自动删除
|
|||
|
|
|
|||
|
|
## 笔记服务 (notes.go)
|
|||
|
|
|
|||
|
|
### 笔记操作
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func SaveNote(db *sql.DB, analysisID int64, scope, scopeKey, content string) (*models.ReviewNote, error)
|
|||
|
|
func GetNotes(db *sql.DB, analysisID int64, scope string) ([]models.ReviewNote, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 作用域类型
|
|||
|
|
|
|||
|
|
- `overall`: 整体审查笔记
|
|||
|
|
- `file`: 文件级别笔记(scope_key = 文件名)
|
|||
|
|
- `suggestion`: 建议级别笔记(scope_key = 建议 ID)
|
|||
|
|
|
|||
|
|
## 错误处理
|
|||
|
|
|
|||
|
|
### 错误传播
|
|||
|
|
|
|||
|
|
- 服务层返回 error
|
|||
|
|
- 处理器层转换为 HTTP 状态码
|
|||
|
|
- SSE 端点通过 error 事件发送
|
|||
|
|
|
|||
|
|
### 常见错误
|
|||
|
|
|
|||
|
|
- 仓库不存在
|
|||
|
|
- LLM API 密钥未配置
|
|||
|
|
- LLM 调用失败
|
|||
|
|
- 差异过大
|
|||
|
|
- 数据库操作失败
|
|||
|
|
|
|||
|
|
## 性能优化
|
|||
|
|
|
|||
|
|
### 并发审查
|
|||
|
|
|
|||
|
|
- 信号量控制并发数
|
|||
|
|
- 线程安全的回调
|
|||
|
|
- 避免数据竞争
|
|||
|
|
|
|||
|
|
### 差异截断
|
|||
|
|
|
|||
|
|
- 单文件: 30k 字符
|
|||
|
|
- PR 生成: 60k 字符
|
|||
|
|
- 避免 token 超限
|
|||
|
|
|
|||
|
|
### 增量渲染
|
|||
|
|
|
|||
|
|
- 按文件分批
|
|||
|
|
- 异步处理
|
|||
|
|
- 进度反馈
|