Files
PR-Helper/docs/02-backend-services.md
T

420 lines
9.5 KiB
Markdown
Raw Normal View History

# 后端服务层
## 概述
后端服务层是 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 超限
### 增量渲染
- 按文件分批
- 异步处理
- 进度反馈