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