docs: 添加 10 份技术文档,README 改为中文

- 01-architecture.md: 架构概览
- 02-backend-services.md: 后端服务层
- 03-frontend-interaction.md: 前端交互设计
- 04-database-design.md: 数据库设计
- 05-api-reference.md: API 接口文档
- 06-sse-streaming.md: SSE 流式传输
- 07-llm-integration.md: LLM 集成
- 08-deployment.md: 部署运维
- 09-development-guide.md: 开发指南
- 10-troubleshooting.md: 故障排查
This commit is contained in:
2026-06-23 22:38:43 +08:00
parent 6f0fabf934
commit 275e5cc886
11 changed files with 4877 additions and 87 deletions
+125 -87
View File
@@ -1,135 +1,173 @@
# PR-Helper
AI-powered PR description generator and code review tool. Self-hosted, designed for internal/local use.
AI 驱动的 PR 描述生成器和代码审查工具。自托管设计,专为内部/本地使用。
## Features
## 功能特性
- **PR Description Generation** — Select commits from an interactive Git graph, get a structured PR description via LLM
- **AI Code Review** — Per-file analysis with severity ratings, inline suggestions on the diff view
- **Interactive Git Graph** — D3.js visualization with click-to-select base/head refs
- **Diff Viewer** — Split/unified view powered by diff2html with syntax highlighting
- **Review Notes** — Add markdown notes at overall, file, or suggestion level
- **PDF Export** — Generate printable review reports
- **Repository Caching** — Cloned repos are cached with configurable expiry
- **PR 描述生成** — 从交互式 Git 图形中选择提交,通过 LLM 生成结构化 PR 描述
- **AI 代码审查** — 按文件分析,严重程度评级,在差异视图中内联显示建议
- **交互式 Git 图形** — D3.js 可视化,点击选择 base/head 引用
- **差异查看器** — diff2html 驱动的分割/统一视图,支持语法高亮
- **审查笔记** — 在整体、文件或建议级别添加 Markdown 笔记
- **PDF 导出** — 生成可打印的审查报告
- **仓库缓存** — 克隆的仓库缓存,可配置过期时间
## Quick Start
## 快速开始
### Docker (recommended)
### Docker(推荐)
```bash
docker compose up --build
```
Open http://localhost:8080.
打开 http://localhost:8080。
### Manual Build
### 手动构建
Requirements: Go 1.22+, CGO enabled (for SQLite), Chromium (for PDF export)
要求:Go 1.22+,CGO 启用(用于 SQLite),Chromium(用于 PDF 导出)
```bash
# Install dependencies
# 安装依赖
go mod download
# Build
# 构建
CGO_ENABLED=1 go build -o pr-helper .
# Run
# 运行
./pr-helper
```
Server listens on `:8080` by default.
服务器默认监听 `:8080` 端口。
## Configuration
## 配置
### LLM Settings (required)
### LLM 设置(必需)
Navigate to **Settings** (`/settings`) and configure:
导航到 **设置** (`/settings`) 并配置:
| Setting | Description | Default |
|---------|-------------|---------|
| API Endpoint | OpenAI-compatible API URL | `https://api.openai.com/v1` |
| API Key | Your API key | (empty) |
| Model | Model name | `gpt-4o` |
| 设置 | 说明 | 默认值 |
|------|------|--------|
| API 端点 | OpenAI 兼容 API URL | `https://api.openai.com/v1` |
| API 密钥 | 您的 API 密钥 | (空) |
| 模型 | 模型名称 | `gpt-4o` |
Works with any OpenAI-compatible API: OpenAI, Deepseek, Ollama, vLLM, etc.
支持任何 OpenAI 兼容 API:OpenAI、Deepseek、Ollama、vLLM 等。
### Review Settings
### 审查设置
| Setting | Description | Default |
|---------|-------------|---------|
| Top-N Files | Max files to analyze per review (0 = all) | `20` |
| Concurrency | Parallel file analyses | `5` |
| 设置 | 说明 | 默认值 |
|------|------|--------|
| Top-N 文件数 | 每次审查分析的最大文件数 (0 = 全部) | `20` |
| 并发数 | 并行文件分析数 | `5` |
### Cache Settings
### 缓存设置
| Setting | Description | Default |
|---------|-------------|---------|
| Max Age (days) | Auto-cleanup threshold | `7` |
| Max Size (MB) | Total cache size limit | `5000` |
| 设置 | 说明 | 默认值 |
|------|------|--------|
| 最大保留天数 | 自动清理阈值 | `7` |
| 最大缓存大小 (MB) | 总缓存大小限制 | `5000` |
### Environment Variables
### 环境变量
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | `8080` |
| `GIN_MODE` | Gin mode (`debug`/`release`) | `debug` |
| `DATA_DIR` | Data directory path | `./data` |
| `CHROME_BIN` | Chromium binary path (for PDF) | `/usr/bin/chromium-browser` |
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `PORT` | 服务器端口 | `8080` |
| `GIN_MODE` | Gin 模式 (`debug`/`release`) | `debug` |
| `DATA_DIR` | 数据目录路径 | `./data` |
| `MYSQL_HOST` | MySQL 主机地址 | `127.0.0.1` |
| `MYSQL_PORT` | MySQL 端口 | `3306` |
| `MYSQL_USER` | MySQL 用户名 | `root` |
| `MYSQL_PASSWORD` | MySQL 密码 | (空) |
| `MYSQL_DATABASE` | MySQL 数据库名 | `pr_helper` |
## Usage
## 使用方法
1. **Clone a repo** — Paste a Git URL on the homepage, optionally provide credentials for private repos
2. **Browse the graph** — View branches, tags, and commit history in the interactive D3.js graph
3. **Select refs** — Click nodes in the graph or use the dropdown selectors to pick base and head
4. **Generate PR description** — Navigate to Generate, select refs, click Generate
5. **Run code review** — Navigate to Review, configure Top-N and concurrency, click Start Review
6. **Add notes** — Click into any suggestion or the overall section to add markdown notes
7. **Export PDF** — Click Export PDF to download a formatted report
1. **克隆仓库** — 在首页粘贴 Git URL,可选提供私有仓库的认证信息
2. **浏览图形** — 在交互式 D3.js 图形中查看分支、标签和提交历史
3. **选择引用** — 点击图形中的节点或使用下拉选择器选择 base 和 head
4. **生成 PR 描述** — 导航到"生成",选择引用,点击"生成"
5. **运行代码审查** — 导航到"审查",配置 Top-N 和并发数,点击"开始审查"
6. **添加笔记** — 点击任何建议或整体部分添加 Markdown 笔记
7. **导出 PDF** — 点击"导出 PDF"下载格式化报告
## API Routes
## API 路由
### Pages
### 页面
| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | Homepage — clone form and cached repos |
| GET | `/repo/:id` | Repository — Git graph and diff viewer |
| GET | `/repo/:id/generate` | PR description generation |
| GET | `/repo/:id/review` | AI code review |
| GET | `/settings` | Settings page |
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/` | 首页 — 克隆表单和已缓存仓库 |
| GET | `/repo/:id` | 仓库详情 — Git 图形和差异查看器 |
| GET | `/settings` | 设置页面 |
### API
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/repos` | Clone repository (SSE stream) |
| GET | `/api/repos` | List cached repositories |
| DELETE | `/api/repos/:id` | Delete repository cache |
| POST | `/api/repos/:id/cleanup` | Trigger cache cleanup |
| GET | `/api/repos/:id/graph` | Git graph data (JSON) |
| GET | `/api/repos/:id/refs` | Branches and tags |
| GET | `/api/repos/:id/commits` | Commit log for a ref |
| GET | `/api/repos/:id/diff` | Diff between two refs |
| POST | `/api/repos/:id/generate` | Generate PR description (SSE) |
| POST | `/api/repos/:id/review` | AI code review (SSE) |
| GET | `/api/repos/:id/review/analyses` | List past reviews |
| GET | `/api/repos/:id/review/analyses/:aid` | Get single review |
| POST | `/api/repos/:id/review/notes` | Save a review note |
| GET | `/api/repos/:id/review/notes` | Get review notes |
| POST | `/api/repos/:id/review/pdf` | Generate PDF report |
| GET | `/api/settings` | Get settings |
| PUT | `/api/settings` | Update settings |
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/repos` | 克隆仓库 (SSE 流) |
| GET | `/api/repos` | 列出已缓存仓库 |
| DELETE | `/api/repos/:id` | 删除仓库缓存 |
| POST | `/api/repos/:id/cleanup` | 触发缓存清理 |
| POST | `/api/repos/:id/pull` | 拉取更新 |
| GET | `/api/repos/:id/graph` | Git 图形数据 (JSON) |
| GET | `/api/repos/:id/refs` | 分支和标签 |
| GET | `/api/repos/:id/commits` | 提交日志 |
| GET | `/api/repos/:id/diff` | 两个引用之间的差异 |
| POST | `/api/repos/:id/generate` | 生成 PR 描述 (SSE) |
| POST | `/api/repos/:id/review` | AI 代码审查 (SSE) |
| GET | `/api/repos/:id/review/analyses` | 列出历史审查 |
| GET | `/api/repos/:id/review/analyses/:aid` | 获取单个审查 |
| POST | `/api/repos/:id/review/notes` | 保存审查笔记 |
| GET | `/api/repos/:id/review/notes` | 获取审查笔记 |
| GET | `/api/settings` | 获取设置 |
| PUT | `/api/settings` | 更新设置 |
## Tech Stack
## 技术栈
- **Backend**: Go, Gin, SQLite (go-sqlite3), go-git, chromedp
- **Frontend**: Go html/template, HTMX, D3.js, diff2html, Tailwind CSS
- **LLM**: OpenAI-compatible API with SSE streaming
| 层级 | 技术 |
|------|------|
| 后端 | Go、Gin、MySQL、go-git |
| 前端 | Go html/template、HTMX、D3.js、diff2html、Tailwind CSS |
| LLM | OpenAI 兼容 API (SSE 流式传输) |
| 部署 | Docker |
## Security
## 项目结构
**No authentication.** Do not expose to the public internet. Intended for internal/local use only.
```
PR-Helper/
├── main.go # 应用入口
├── config/ # 配置加载
├── database/ # 数据库初始化和迁移
├── handlers/ # HTTP 处理器 (页面 + JSON API + SSE)
├── models/ # 数据模型
├── services/ # 业务逻辑 (Git 操作、LLM 调用、缓存管理)
├── templates/ # Go HTML 模板
├── static/ # CSS (Tailwind)、JS、第三方库
├── data/ # 克隆的仓库缓存
└── docs/ # 技术文档
```
## License
## 技术文档
详细的技术文档位于 [docs/](docs/) 目录:
| 文档 | 说明 |
|------|------|
| [01-architecture.md](docs/01-architecture.md) | 架构概览 — 项目结构、技术栈、数据流 |
| [02-backend-services.md](docs/02-backend-services.md) | 后端服务层 — Git、LLM、PR 生成、代码审查 |
| [03-frontend-interaction.md](docs/03-frontend-interaction.md) | 前端交互设计 — SSE、D3.js 图形、差异查看器 |
| [04-database-design.md](docs/04-database-design.md) | 数据库设计 — 表结构、索引、迁移策略 |
| [05-api-reference.md](docs/05-api-reference.md) | API 接口文档 — 完整的 RESTful API 参考 |
| [06-sse-streaming.md](docs/06-sse-streaming.md) | SSE 流式传输 — 协议、实现、错误处理 |
| [07-llm-integration.md](docs/07-llm-integration.md) | LLM 集成 — 配置、提示模板、流式调用 |
| [08-deployment.md](docs/08-deployment.md) | 部署运维 — Docker、反向代理、监控 |
| [09-development-guide.md](docs/09-development-guide.md) | 开发指南 — 环境搭建、代码规范、测试 |
| [10-troubleshooting.md](docs/10-troubleshooting.md) | 故障排查 — 常见问题和解决方案 |
## 安全
**无认证机制。** 请勿暴露到公网。仅限内部/本地使用。
## 许可证
MIT
+163
View File
@@ -0,0 +1,163 @@
# 架构概览
## 项目简介
PR-Helper 是一个自托管的 Web 服务,用于从 Git 历史自动生成 PR 描述并执行 AI 代码审查。专为内部/本地使用设计(无认证)。
## 技术栈
| 层级 | 技术 |
|------|------|
| 后端 | Go + Gin + MySQL + go-git |
| 前端 | Go html/template + HTMX + D3.js + diff2html + Tailwind CSS |
| LLM | OpenAI 兼容 API(SSE 流式传输) |
| 部署 | Docker |
## 目录结构
```
PR-Helper/
├── main.go # 应用入口
├── config/ # 配置加载
│ └── config.go
├── database/ # 数据库初始化和迁移
│ └── db.go
├── handlers/ # HTTP 处理器(页面 + JSON API + SSE)
│ ├── auth.go # 认证处理
│ ├── generate.go # PR 生成 SSE 端点
│ ├── pages.go # 页面渲染
│ ├── repos.go # 仓库管理 API
│ ├── review.go # 代码审查 SSE 端点
│ ├── settings.go # 设置 API
│ └── middleware.go # 中间件
├── models/ # 数据模型
│ ├── repository.go # 仓库模型
│ ├── analysis.go # 分析结果模型
│ └── settings.go # 默认设置
├── services/ # 业务逻辑
│ ├── git.go # Git 操作(克隆、差异、图形数据)
│ ├── llm.go # LLM API 调用(SSE 流式)
│ ├── generate.go # PR 描述生成
│ ├── review.go # AI 代码审查
│ ├── cache.go # 仓库缓存管理
│ └── notes.go # 审查笔记
├── templates/ # Go HTML 模板
│ ├── layouts/ # 布局模板
│ ├── pages/ # 页面模板
│ └── partials/ # 局部模板
├── static/ # 静态资源
│ ├── css/ # Tailwind 输出
│ ├── js/ # 自定义 JS
│ └── lib/ # 第三方库
└── data/ # 数据存储
└── repos/ # 克隆的仓库缓存
```
## 数据流
```
浏览器 ↔ Gin 处理器 → 服务层(git/llm)→ MySQL + 文件系统(data/)
```
### 请求流程
1. **浏览器** 发起 HTTP 请求(HTMX 或 fetch)
2. **Gin 路由** 匹配处理器函数
3. **中间件** 验证会话和用户认证
4. **处理器** 解析请求参数
5. **服务层** 执行业务逻辑
6. **数据库/文件系统** 持久化数据
7. **SSE 流** 返回实时更新(可选)
## 核心模块
### 配置模块 (config/)
- 从 `.env` 文件加载环境变量
- 提供默认值
- 生成随机会话密钥(如未配置)
### 数据库模块 (database/)
- MySQL 连接管理
- 自动迁移(CREATE TABLE IF NOT EXISTS)
- 增量迁移(ALTER TABLE,忽略重复列错误)
- 默认设置初始化
### 处理器模块 (handlers/)
- **页面处理器**: 渲染 HTML 模板
- **仓库处理器**: 克隆、删除、拉取、获取图形/差异
- **生成处理器**: PR 描述生成(SSE 流式)
- **审查处理器**: AI 代码审查(SSE 流式)
- **设置处理器**: 读取/更新用户设置
### 服务模块 (services/)
- **git.go**: 使用 go-git 库执行 Git 操作
- **llm.go**: OpenAI 兼容 API 调用,支持流式响应
- **generate.go**: 从提交历史和差异生成 PR 描述
- **review.go**: Top-N 策略的 AI 代码审查
- **cache.go**: 仓库缓存生命周期管理
### 前端模块 (static/js/)
- **sse.js**: SSE 客户端,支持 POST 请求
- **graph.js**: D3.js Git 图形可视化
- **diff-viewer.js**: diff2html 差异查看器
- **review-inline.js**: 内联 AI 建议
- **markdown.js**: Markdown 渲染
- **note-editor.js**: 审查笔记编辑器
## 关键设计决策
### 1. SSE 流式传输
选择 SSE 而非 WebSocket 的原因:
- 单向服务器到客户端通信足够
- HTTP 兼容性更好
- 实现更简单
- 自动重连
### 2. Top-N 策略
大型差异处理:
- 按变更行数排序文件
- 只分析前 N 个文件(默认 20)
- 每个文件独立分析
- 最后生成汇总
### 3. 三级审查笔记
- `overall`: 整体审查笔记
- `file`: 文件级别笔记
- `suggestion`: 建议级别笔记
### 4. 会话认证
- 基于 Cookie 的会话
- 7 天过期
- HttpOnly 标记
- 随机会话密钥
## 扩展性考虑
### 水平扩展
- 无状态设计(会话存储在 Cookie)
- 数据库可外部化
- 文件存储可替换为对象存储
### LLM 集成
- 支持任何 OpenAI 兼容 API
- 每用户独立配置
- 可扩展为多模型支持
## 安全考虑
- 仅限内部使用(无认证)
- 不暴露到公网
- SQL 参数化查询
- 会话 HttpOnly
- 凭证不明文存储在 JSON 响应中
+419
View File
@@ -0,0 +1,419 @@
# 后端服务层
## 概述
后端服务层是 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 超限
### 增量渲染
- 按文件分批
- 异步处理
- 进度反馈
+339
View File
@@ -0,0 +1,339 @@
# 前端交互设计
## 概述
PR-Helper 前端采用渐进式增强设计,结合 HTMX、D3.js、diff2html 等库,提供流畅的交互体验。
## 技术栈
| 技术 | 用途 |
|------|------|
| Go html/template | 服务端渲染 |
| HTMX 2.x | 动态交互 |
| D3.js 7.x | Git 图形可视化 |
| diff2html 3.x | 差异渲染 |
| highlight.js 11.x | 语法高亮 |
| marked.js | Markdown 渲染 |
| Tailwind CSS | 样式框架 |
## 核心组件
### 1. SSE 客户端 (sse.js)
#### 设计目标
- 支持 POST 请求的 SSE(EventSource 只支持 GET)
- 流式读取响应体
- 事件分发机制
#### API 设计
```javascript
const SSE = {
async post(url, body, handlers = {}) {
// 返回 controller,支持 abort()
return { abort: () => controller.abort() };
}
};
// 使用示例
SSE.post('/api/repos/1/review', { base: 'main', head: 'feature' }, {
start: (data) => console.log('开始', data),
content: (data) => console.log('内容', data.content),
suggestion: (data) => console.log('建议', data),
summary: (data) => console.log('汇总', data),
done: () => console.log('完成'),
error: (data) => console.error('错误', data.message)
});
```
#### 实现细节
- 使用 `fetch()` + `ReadableStream`
- 手动解析 SSE 协议(event: / data:)
- 支持中断请求
- 自动处理 JSON 解析
### 2. Git 图形 (graph.js)
#### 功能特性
- D3.js SVG 渲染
- 多分支彩色泳道
- 交互式 base/head 选择
- 分支标签右对齐
- 图例显示
#### 布局算法
```javascript
assignLanes() {
// 1. HEAD 分支获得泳道 0
// 2. 其他分支按顺序分配泳道
// 3. 合并提交的第二父提交获得新泳道
// 4. 未访问的提交分配到泳道 0
}
```
#### 交互设计
- **点击**: 选择 base/head 提交
- **悬停**: 高亮节点
- **选择状态**:
- 绿色: base 提交
- 橙色: head 提交
- 蓝色: 普通提交
#### 回调机制
```javascript
gitGraph.onSelectionChange = (baseHash, headHash) => {
// 更新 base/head 选择
// 触发差异加载
};
```
### 3. 差异查看器 (diff-viewer.js)
#### 功能特性
- diff2html 渲染
- 文件树侧边栏
- 增量渲染(非阻塞)
- 懒加载语法高亮
- 滚动同步
- 大型差异保护
#### 增量渲染
```javascript
_renderBatch(outputFormat, startIndex) {
const FILES_PER_BATCH = 5;
// 每帧渲染 5 个文件
// 使用 requestAnimationFrame
// 避免阻塞主线程
}
```
#### 大型差异保护
```javascript
_isTooLarge(files) {
const MAX_FILES = 300;
const MAX_LINES = 10000;
return files.length > MAX_FILES || (totalAdd + totalDel) > MAX_LINES;
}
```
- 超过阈值显示警告
- 用户确认后强制渲染
- 避免浏览器卡顿
#### 文件树
- 目录折叠/展开
- 文件类型图标
- 变更统计(+/-)
- 严重程度标记
#### 滚动同步
```javascript
_setupScrollSpy() {
// IntersectionObserver 监听文件可见性
// 自动高亮侧边栏对应文件
// 点击侧边栏滚动到文件
}
```
### 4. 内联建议 (review-inline.js)
#### 功能
- 在差异中嵌入 AI 建议
- 按严重程度着色
- 支持文件级和行级建议
#### 插入机制
```javascript
insertSuggestion(filename, line, side, severity, content, suggestionId) {
// 1. 懒加载文件索引
// 2. 查找目标行
// 3. 创建建议卡片
// 4. 插入到行后
}
```
#### 严重程度样式
```javascript
const severityStyles = {
critical: 'border-l-4 border-red-500 bg-red-50',
warning: 'border-l-4 border-yellow-500 bg-yellow-50',
info: 'border-l-4 border-green-500 bg-green-50',
};
```
### 5. Markdown 渲染 (markdown.js)
#### 功能
- 使用 marked.js 渲染
- 代码块语法高亮
- 支持内联代码
#### 集成
```javascript
function renderMarkdown(text) {
return marked.parse(text, {
highlight: function(code, lang) {
return hljs.highlightAuto(code).value;
}
});
}
```
### 6. 笔记编辑器 (note-editor.js)
#### 功能
- 内联编辑审查笔记
- 自动保存
- 三种作用域
## 页面结构
### 首页 (/)
- 仓库列表
- 克隆表单
- 快速操作
### 仓库详情页 (/repo/:id)
- Git 图形
- 分支选择器
- 差异查看器
- PR 生成
- AI 代码审查
### 设置页 (/settings)
- LLM 配置
- 审查参数
- 缓存设置
## HTMX 集成
### 动态加载
```html
<div hx-get="/api/repos/1/graph" hx-trigger="load">
<!-- 图形将在这里渲染 -->
</div>
```
### 表单提交
```html
<form hx-post="/api/repos" hx-target="#repo-list">
<input type="text" name="url" required>
<button type="submit">克隆</button>
</form>
```
### 事件触发
```html
<button hx-post="/api/repos/1/pull"
hx-trigger="click"
hx-indicator="#spinner">
拉取更新
</button>
```
## 响应式设计
### 断点
- 移动端: < 640px
- 平板: 640px - 1024px
- 桌面: > 1024px
### 适配策略
- 文件树侧边栏可折叠
- 差异查看器自适应宽度
- 图形可横向滚动
## 性能优化
### 1. 增量渲染
- 分批渲染文件
- requestAnimationFrame
- 避免长任务阻塞
### 2. 懒加载
- 语法高亮按需执行
- 文件索引按需构建
- IntersectionObserver
### 3. 虚拟滚动
- 大型差异只渲染可见部分
- 减少 DOM 节点数
### 4. 缓存
- 静态资源缓存
- API 响应缓存
- 本地状态缓存
## 错误处理
### 网络错误
- 显示错误消息
- 提供重试按钮
- 自动重连(SSE)
### 渲染错误
- 降级到纯文本
- 显示原始数据
- 记录错误日志
## 无障碍
### 键盘导航
- Tab 切换焦点
- Enter 确认选择
- Escape 取消操作
### 屏幕阅读器
- ARIA 标签
- 语义化 HTML
- 焦点管理
## 浏览器兼容性
### 支持的浏览器
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
### 依赖的现代 API
- fetch
- ReadableStream
- IntersectionObserver
- requestAnimationFrame
- CSS Grid/Flexbox
+394
View File
@@ -0,0 +1,394 @@
# 数据库设计
## 概述
PR-Helper 使用 MySQL 作为主数据库,存储用户信息、仓库元数据、分析结果和审查笔记。
## 数据库配置
### 连接参数
```env
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_DATABASE=pr_helper
```
### DSN 格式
```
{user}:{password}@tcp({host}:{port})/{database}?charset=utf8mb4&parseTime=True&loc=Local
```
## 表结构
### 1. settings 表
全局键值存储,保存系统配置。
```sql
CREATE TABLE IF NOT EXISTS settings (
`key` VARCHAR(255) PRIMARY KEY,
value TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
| 字段 | 类型 | 说明 |
|------|------|------|
| key | VARCHAR(255) | 配置键名(主键) |
| value | TEXT | 配置值 |
#### 默认设置
```go
var DefaultSettings = map[string]string{
"llm.endpoint": "https://api.deepseek.com",
"llm.api_key": "",
"llm.model": "deepseek-v4-pro",
"review.top_n": "20",
"review.concurrency": "5",
"cache.max_age_days": "7",
"cache.max_size_mb": "5000",
}
```
### 2. users 表
用户账户信息。
```sql
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 用户 ID(主键,自增) |
| email | VARCHAR(255) | 邮箱(唯一) |
| password_hash | TEXT | 密码哈希 |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |
### 3. user_settings 表
用户个人设置,覆盖全局默认值。
```sql
CREATE TABLE IF NOT EXISTS user_settings (
user_id BIGINT NOT NULL,
`key` VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (user_id, `key`),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
| 字段 | 类型 | 说明 |
|------|------|------|
| user_id | BIGINT | 用户 ID(外键) |
| key | VARCHAR(255) | 配置键名 |
| value | TEXT | 配置值 |
#### 常用配置键
| 键名 | 说明 | 默认值 |
|------|------|--------|
| llm.endpoint | LLM API 端点 | https://api.deepseek.com |
| llm.api_key | LLM API 密钥 | (空) |
| llm.model | LLM 模型名称 | deepseek-v4-pro |
| review.top_n | 审查文件数上限 | 20 |
| review.concurrency | 审查并发数 | 5 |
### 4. repositories 表
克隆的仓库元数据。
```sql
CREATE TABLE IF NOT EXISTS repositories (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
url TEXT NOT NULL,
local_path TEXT NOT NULL,
size_bytes BIGINT DEFAULT 0,
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used DATETIME DEFAULT CURRENT_TIMESTAMP,
auth_type VARCHAR(20) NOT NULL DEFAULT 'none',
credential TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 仓库 ID(主键,自增) |
| user_id | BIGINT | 所有者用户 ID(外键) |
| url | TEXT | 仓库 URL |
| local_path | TEXT | 本地存储路径 |
| size_bytes | BIGINT | 仓库大小(字节) |
| cloned_at | DATETIME | 克隆时间 |
| last_used | DATETIME | 最后使用时间 |
| auth_type | VARCHAR(20) | 认证类型(none/https/ssh) |
| credential | TEXT | 认证凭证(不明文返回) |
### 5. analyses 表
分析结果存储(PR 描述和代码审查)。
```sql
CREATE TABLE IF NOT EXISTS analyses (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
repo_id BIGINT,
type VARCHAR(50) NOT NULL,
base_ref TEXT NOT NULL,
head_ref TEXT NOT NULL,
result LONGTEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (repo_id) REFERENCES repositories(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 分析 ID(主键,自增) |
| user_id | BIGINT | 用户 ID(外键) |
| repo_id | BIGINT | 仓库 ID(外键) |
| type | VARCHAR(50) | 类型(pr_description/code_review) |
| base_ref | TEXT | 基准分支 |
| head_ref | TEXT | 目标分支 |
| result | LONGTEXT | 结果 JSON |
| created_at | DATETIME | 创建时间 |
#### result 字段格式
**PR 描述类型**:
```json
{
"content": "# 标题\n\n## 概述\n..."
}
```
**代码审查类型**:
```json
{
"file_reviews": [
{
"file_name": "main.go",
"change_lines": 42,
"suggestions": [
{
"severity": "warning",
"description": "...",
"suggestion": "...",
"code_example": "..."
}
],
"raw_review": "..."
}
],
"summary": {
"score": 7,
"overall": "...",
"findings": "...",
"recommendations": "..."
},
"top_n": 20
}
```
### 6. review_notes 表
用户添加的审查笔记。
```sql
CREATE TABLE IF NOT EXISTS review_notes (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
analysis_id BIGINT,
scope VARCHAR(50) NOT NULL,
scope_key TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (analysis_id) REFERENCES analyses(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 笔记 ID(主键,自增) |
| user_id | BIGINT | 用户 ID(外键) |
| analysis_id | BIGINT | 分析 ID(外键) |
| scope | VARCHAR(50) | 作用域类型 |
| scope_key | TEXT | 作用域键 |
| content | TEXT | 笔记内容 |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |
#### scope 类型
| scope | scope_key | 说明 |
|-------|-----------|------|
| overall | (空) | 整体审查笔记 |
| file | 文件名 | 文件级别笔记 |
| suggestion | 建议 ID | 建议级别笔记 |
## 索引设计
### 主键索引
- 所有表使用 BIGINT 自增主键
- settings 表使用 key 作为主键
### 唯一索引
- users.email: 唯一约束
### 复合主键
- user_settings: (user_id, key)
### 外键索引
- user_settings.user_id → users.id
- repositories.user_id → users.id
- analyses.user_id → users.id
- analyses.repo_id → repositories.id
- review_notes.user_id → users.id
- review_notes.analysis_id → analyses.id
## 迁移策略
### 自动迁移
```go
func (db *DB) migrate() error {
stmts := []string{
"CREATE TABLE IF NOT EXISTS ...",
// ...
}
for _, s := range stmts {
if _, err := db.conn.Exec(s); err != nil {
return err
}
}
return nil
}
```
### 增量迁移
```go
alterStmts := []string{
"ALTER TABLE repositories ADD COLUMN auth_type VARCHAR(20) NOT NULL DEFAULT 'none'",
"ALTER TABLE repositories ADD COLUMN credential TEXT",
}
for _, s := range alterStmts {
db.conn.Exec(s) // 忽略错误(列已存在)
}
```
- 使用 `IF NOT EXISTS` 避免重复创建
- 忽略 "duplicate column" 错误实现幂等性
### 默认数据初始化
```go
func (db *DB) seedDefaults() error {
for key, val := range models.DefaultSettings {
_, err := db.conn.Exec(
"INSERT IGNORE INTO settings (`key`, value) VALUES (?, ?)", key, val,
)
// ...
}
return nil
}
```
- 使用 `INSERT IGNORE` 避免重复插入
## 数据完整性
### 外键约束
- 级联删除(ON DELETE CASCADE)
- 删除用户时自动清理相关数据
### 数据验证
- 应用层验证(handler 层)
- 数据库约束(NOT NULL, UNIQUE)
## 性能优化
### 查询优化
- 使用参数化查询避免 SQL 注入
- 避免 SELECT *,只查询需要的字段
- 合理使用索引
### 连接管理
- 使用 database/sql 连接池
- 设置合理的连接超时
- 及时关闭连接
### 大字段处理
- result 使用 LONGTEXT
- 考虑分表或外部存储(未来优化)
## 备份策略
### 逻辑备份
```bash
mysqldump -u root -p pr_helper > backup.sql
```
### 恢复
```bash
mysql -u root -p pr_helper < backup.sql
```
### 定期备份
- 建议每日备份
- 保留最近 7 天备份
- 异地备份(生产环境)
## 监控
### 关键指标
- 连接数
- 查询耗时
- 慢查询
- 表大小
### 命令
```sql
-- 查看连接数
SHOW STATUS LIKE 'Threads_connected';
-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log';
-- 查看表大小
SELECT table_name, round(((data_length + index_length) / 1024 / 1024), 2) AS "Size (MB)"
FROM information_schema.tables
WHERE table_schema = 'pr_helper';
```
+660
View File
@@ -0,0 +1,660 @@
# API 接口文档
## 概述
PR-Helper 提供 RESTful JSON API 和 SSE 流式端点。所有 API 都需要认证(除登录/注册外)。
## 认证
### 会话认证
- 基于 Cookie 的会话
- Cookie 名称: `pr_session`
- 有效期: 7 天
- HttpOnly: true
### 认证头
所有受保护的 API 需要有效的会话 Cookie。
## 公开端点
### 登录页面
```
GET /login
```
返回登录页面 HTML。
### 注册页面
```
GET /register
```
返回注册页面 HTML。
### 用户登录
```
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
```
**响应**:
```json
{
"message": "登录成功"
}
```
**错误**:
```json
{
"error": "邮箱或密码错误"
}
```
### 用户注册
```
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
```
**响应**:
```json
{
"message": "注册成功"
}
```
### 用户登出
```
POST /api/auth/logout
```
**响应**:
```json
{
"message": "已登出"
}
```
## 页面端点
### 首页
```
GET /
```
返回首页 HTML(仓库列表)。
### 仓库详情页
```
GET /repo/:id
```
返回仓库详情页 HTML(Git 图形、差异查看器等)。
### 设置页
```
GET /settings
```
返回设置页面 HTML。
## 仓库管理 API
### 获取仓库列表
```
GET /api/repos
```
**响应**:
```json
[
{
"id": 1,
"url": "https://github.com/user/repo.git",
"local_path": "data/repos/user_repo.git_1234567890",
"size_bytes": 1048576,
"cloned_at": "2024-01-01T00:00:00Z",
"last_used": "2024-01-01T12:00:00Z",
"auth_type": "none"
}
]
```
### 克隆仓库
```
POST /api/repos
Content-Type: application/json
{
"url": "https://github.com/user/repo.git",
"auth_type": "none",
"credential": ""
}
```
**认证类型**:
- `none`: 无认证
- `https`: HTTPS 基本认证(credential = "username:password")
**响应 (SSE 流)**:
```
event: progress
data: {"step":"开始克隆","current":0,"total":0}
event: progress
data: {"step":"克隆完成","current":1,"total":1}
event: done
data: {"repo_id":1,"branches":["main","dev"],"tags":["v1.0"],"commit_num":100}
```
### 删除仓库
```
DELETE /api/repos/:id
```
**响应**:
```json
{
"message": "仓库已删除"
}
```
### 清理仓库缓存
```
POST /api/repos/:id/cleanup
```
删除并重新克隆仓库。
**响应 (SSE 流)**:
```
event: progress
data: {"step":"清理完成"}
event: progress
data: {"step":"开始克隆"}
event: done
data: {"repo_id":1}
```
### 拉取更新
```
POST /api/repos/:id/pull
```
**响应 (SSE 流)**:
```
event: progress
data: {"step":"拉取完成"}
event: done
data: {"commit_num":105,"branches":["main","dev"]}
```
### 获取 Git 图形
```
GET /api/repos/:id/graph
```
**响应**:
```json
{
"commits": [
{
"hash": "abc123...",
"short_hash": "abc1234",
"message": "feat: add new feature",
"author": "John Doe",
"email": "john@example.com",
"timestamp": "2024-01-01T12:00:00Z",
"parent_ids": ["def456..."]
}
],
"refs": [
{
"name": "main",
"hash": "abc123...",
"is_head": true,
"is_tag": false
},
{
"name": "v1.0",
"hash": "abc123...",
"is_head": false,
"is_tag": true
}
],
"edges": [
{
"source": "abc123...",
"target": "def456..."
}
]
}
```
### 获取引用列表
```
GET /api/repos/:id/refs
```
**响应**:
```json
[
{
"name": "main",
"hash": "abc123...",
"is_head": true,
"is_tag": false
},
{
"name": "v1.0",
"hash": "abc123...",
"is_head": false,
"is_tag": true
}
]
```
### 获取提交列表
```
GET /api/repos/:id/commits?ref=main&limit=50
```
**参数**:
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| ref | string | 否 | 分支/标签名(默认 HEAD) |
| limit | int | 否 | 最大提交数(默认 50) |
**响应**:
```json
[
{
"hash": "abc123...",
"short_hash": "abc1234",
"message": "feat: add new feature",
"author": "John Doe",
"email": "john@example.com",
"timestamp": "2024-01-01T12:00:00Z",
"parent_ids": ["def456..."]
}
]
```
### 获取差异
```
GET /api/repos/:id/diff?base=main&head=feature
```
**参数**:
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| base | string | 是 | 基准分支/标签/提交 |
| head | string | 是 | 目标分支/标签/提交 |
| per_file | bool | 否 | 是否按文件返回 |
**响应 (统一差异)**:
```json
{
"diff": "diff --git a/main.go b/main.go\n..."
}
```
**响应 (按文件)**:
```json
[
{
"filename": "main.go",
"patch": "diff --git a/main.go b/main.go\n..."
}
]
```
## PR 生成 API
### 生成 PR 描述
```
POST /api/repos/:id/generate
Content-Type: application/json
{
"base": "main",
"head": "feature"
}
```
**响应 (SSE 流)**:
```
event: content
data: {"content":"# 标题\n\n"}
event: content
data: {"content":"**类型**: feat\n\n"}
event: content
data: {"content":"## 概述\n..."}
event: done
data: {"content":""}
```
## 代码审查 API
### 执行代码审查
```
POST /api/repos/:id/review
Content-Type: application/json
{
"base": "main",
"head": "feature",
"top_n": 20,
"concurrency": 5
}
```
**参数**:
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| base | string | 是 | 基准分支 |
| head | string | 是 | 目标分支 |
| top_n | int | 否 | 审查文件数上限(默认 20) |
| concurrency | int | 否 | 并发数(默认 5) |
**响应 (SSE 流)**:
```
event: start
data: {"total_files":30,"reviewed_files":20,"top_n":20}
event: file_start
data: {"file":"main.go","index":1,"total":20}
event: content
data: {"content":"..."}
event: suggestion
data: {"file":"main.go","severity":"warning","content":"..."}
event: file_end
data: {"file":"main.go"}
event: summary
data: {"score":7,"overall":"...","findings":"...","recommendations":"..."}
event: analysis_saved
data: {"analysis_id":1}
event: done
data: {"content":""}
```
### 获取审查历史
```
GET /api/repos/:id/review/analyses
```
**响应**:
```json
[
{
"id": 1,
"base_ref": "main",
"head_ref": "feature",
"created_at": "2024-01-01T12:00:00Z"
}
]
```
### 获取单个审查结果
```
GET /api/repos/:id/review/analyses/:aid
```
**响应**:
```json
{
"id": 1,
"base_ref": "main",
"head_ref": "feature",
"created_at": "2024-01-01T12:00:00Z",
"result": {
"file_reviews": [...],
"summary": {...},
"top_n": 20
}
}
```
### 保存审查笔记
```
POST /api/repos/:id/review/notes
Content-Type: application/json
{
"analysis_id": 1,
"scope": "file",
"scope_key": "main.go",
"content": "这个文件需要重构"
}
```
**scope 类型**:
| scope | scope_key | 说明 |
|-------|-----------|------|
| overall | (空) | 整体笔记 |
| file | 文件名 | 文件级笔记 |
| suggestion | 建议 ID | 建议级笔记 |
**响应**:
```json
{
"id": 1,
"analysis_id": 1,
"scope": "file",
"scope_key": "main.go",
"content": "这个文件需要重构",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
```
### 获取审查笔记
```
GET /api/repos/:id/review/notes?analysis_id=1&scope=file
```
**参数**:
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| analysis_id | int | 是 | 分析 ID |
| scope | string | 否 | 过滤作用域 |
**响应**:
```json
[
{
"id": 1,
"analysis_id": 1,
"scope": "file",
"scope_key": "main.go",
"content": "这个文件需要重构",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
]
```
## 设置 API
### 获取设置
```
GET /api/settings
```
**响应**:
```json
{
"llm.endpoint": "https://api.deepseek.com",
"llm.api_key": "sk-...",
"llm.model": "deepseek-v4-pro",
"review.top_n": "20",
"review.concurrency": "5",
"cache.max_age_days": "7",
"cache.max_size_mb": "5000"
}
```
### 更新设置
```
PUT /api/settings
Content-Type: application/json
{
"llm.endpoint": "https://api.openai.com",
"llm.api_key": "sk-...",
"llm.model": "gpt-4"
}
```
**响应**:
```json
{
"message": "设置已更新"
}
```
## 错误响应
### 格式
```json
{
"error": "错误信息"
}
```
### 常见状态码
| 状态码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未认证 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## SSE 协议
### 事件格式
```
event: {event_name}
data: {json_data}
```
- 每个事件以 `event:` 开头
- 数据以 `data:` 开头
- 事件之间用空行分隔
- 数据必须是有效的 JSON
### 客户端实现
```javascript
SSE.post(url, body, {
eventName: (data) => {
// 处理事件
},
error: (data) => {
// 处理错误
},
done: () => {
// 流结束
}
});
```
### 中断请求
```javascript
const controller = SSE.post(url, body, handlers);
// 中断
controller.abort();
```
## 限流
当前无速率限制。建议在反向代理层实现限流。
## CORS
当前不支持跨域请求。仅限同源访问。
+446
View File
@@ -0,0 +1,446 @@
# SSE 流式传输
## 概述
Server-Sent Events (SSE) 是 PR-Helper 的核心通信机制,用于实时推送克隆进度、PR 生成和代码审查结果。
## 为什么选择 SSE
### 对比 WebSocket
| 特性 | SSE | WebSocket |
|------|-----|-----------|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | 独立协议 |
| 实现 | 简单 | 复杂 |
| 重连 | 自动 | 手动 |
| 兼容性 | 好 | 需要升级 |
### 适用场景
- 服务器推送数据到客户端
- 不需要客户端频繁发送数据
- 需要 HTTP 兼容性
- 需要自动重连
## SSE 协议
### 基本格式
```
event: message_type
data: {"key": "value"}
```
- `event:` 事件类型(可选)
- `data:` 事件数据(JSON 字符串)
- 空行分隔事件
### 多行数据
```
event: content
data: {"content": "第一行\n第二行"}
```
## 后端实现
### 设置响应头
```go
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
```
### 发送事件
```go
sendEvent := func(event string, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
jsonData = []byte(`{"error":"序列化事件数据失败"}`)
}
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, jsonData)
flusher.Flush()
}
```
### 获取 Flusher
```go
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器不支持流式传输"})
return
}
```
## 前端实现
### SSE 客户端
```javascript
const SSE = {
async post(url, body, handlers = {}) {
const controller = new AbortController();
const run = async () => {
try {
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body),
signal: controller.signal,
});
if (!resp.ok) {
const errText = await resp.text();
// 处理错误...
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentEvent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('event: ')) {
currentEvent = line.slice(7).trim();
} else if (line.startsWith('data: ')) {
const raw = line.slice(6);
let data;
try {
data = JSON.parse(raw);
} catch (_) {
data = raw;
}
if (currentEvent && handlers[currentEvent]) {
handlers[currentEvent](data);
}
}
}
}
if (handlers.done) handlers.done();
} catch (err) {
if (err.name === 'AbortError') return;
if (handlers.error) handlers.error({ message: err.message });
}
};
run();
return { abort: () => controller.abort() };
}
};
```
### 使用示例
```javascript
SSE.post('/api/repos/1/review', {
base: 'main',
head: 'feature',
top_n: 20,
concurrency: 5
}, {
// 开始事件
start: (data) => {
console.log(`开始审查 ${data.total_files} 个文件`);
updateProgress(0, data.reviewed_files);
},
// 文件开始
file_start: (data) => {
console.log(`正在审查 ${data.file} (${data.index}/${data.total})`);
highlightCurrentFile(data.file);
},
// 内容流
content: (data) => {
appendToOutput(data.content);
},
// 审查建议
suggestion: (data) => {
showSuggestion(data.file, data.severity, data.content);
},
// 文件结束
file_end: (data) => {
markFileComplete(data.file);
},
// 汇总
summary: (data) => {
showSummary(data.score, data.overall, data.findings, data.recommendations);
},
// 分析保存
analysis_saved: (data) => {
console.log(`分析已保存,ID: ${data.analysis_id}`);
},
// 进度
progress: (data) => {
if (data.step === 'generating_summary') {
showSpinner('生成汇总中...');
}
},
// 完成
done: () => {
console.log('审查完成');
hideSpinner();
},
// 错误
error: (data) => {
console.error('错误:', data.message);
showError(data.message);
}
});
```
## 事件类型
### PR 生成事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `content` | `{content: string}` | 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}` | 错误 |
| `analysis_saved` | `{analysis_id}` | 保存完成 |
| `done` | `{content: ""}` | 完成 |
### 克隆事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `progress` | `{step, current, total}` | 进度 |
| `error` | `{message: string}` | 错误 |
| `done` | `{repo_id, branches, tags, commit_num}` | 完成 |
## 错误处理
### 后端错误
```go
if err != nil {
sendEvent("error", map[string]interface{}{
"message": err.Error(),
})
return
}
```
### 前端错误
```javascript
error: (data) => {
// 显示错误消息
showErrorToast(data.message);
// 重置 UI
resetProgressBar();
hideSpinner();
}
```
### 网络错误
```javascript
catch (err) {
if (err.name === 'AbortError') {
// 用户中断,忽略
return;
}
if (handlers.error) {
handlers.error({ message: err.message });
}
}
```
## 中断请求
### 前端中断
```javascript
const controller = SSE.post(url, body, handlers);
// 用户点击取消按钮
cancelButton.onclick = () => {
controller.abort();
};
```
### 后端处理
- 客户端断开连接时,Gin 会检测到
- 服务层应检查 context 取消
- 长时间运行的操作应支持取消
## 性能优化
### 缓冲控制
```
X-Accel-Buffering: no
```
- 禁用 Nginx 缓冲
- 确保事件立即发送
### 批量发送
- 避免频繁发送小事件
- 合并相关数据
### 压缩
- SSE 不支持 gzip 压缩
- 数据量大时考虑压缩正文
## 并发安全
### 线程安全回调
```go
safeCallback := callback
if callback != nil {
safeCallback = func(event string, data interface{}) {
mu.Lock()
defer mu.Unlock()
callback(event, data)
}
}
```
- 使用互斥锁保护回调
- 避免并发写入
### Goroutine 管理
```go
var wg sync.WaitGroup
for i, file := range files {
wg.Add(1)
go func(idx int, f FileDiff) {
defer wg.Done()
// ...
}(i, file)
}
wg.Wait()
```
- 等待所有 goroutine 完成
- 避免资源泄漏
## 测试
### 手动测试
```bash
curl -N -X POST http://localhost:8080/api/repos/1/review \
-H "Content-Type: application/json" \
-d '{"base":"main","head":"feature"}'
```
### 自动化测试
```go
func TestSSEStream(t *testing.T) {
// 创建测试服务器
// 发送请求
// 读取事件流
// 验证事件序列
}
```
## 监控
### 关键指标
- 连接数
- 事件发送速率
- 错误率
- 响应时间
### 日志
```go
log.Printf("SSE connected: %s", c.ClientIP())
log.Printf("SSE event: %s", event)
log.Printf("SSE disconnected: %s", c.ClientIP())
```
## 安全考虑
### 认证
- 所有 SSE 端点需要认证
- 使用会话 Cookie
### 速率限制
- 限制并发 SSE 连接数
- 限制事件发送频率
### 数据验证
- 验证输入参数
- 防止注入攻击
## 故障排查
### 常见问题
1. **事件不发送**
- 检查 Flusher 是否可用
- 确认响应头设置正确
- 检查 Nginx 缓冲配置
2. **连接断开**
- 检查超时设置
- 确认网络稳定
- 查看错误日志
3. **数据乱码**
- 确认 JSON 序列化正确
- 检查字符编码
- 验证事件格式
### 调试工具
- 浏览器开发者工具 Network 面板
- curl 命令行测试
- Wireshark 抓包
+481
View File
@@ -0,0 +1,481 @@
# LLM 集成
## 概述
PR-Helper 集成了 OpenAI 兼容的 LLM API,用于自动生成 PR 描述和执行 AI 代码审查。支持流式响应,提供实时反馈。
## 架构
```
前端 → SSE 客户端 → Handler → LLM 服务 → OpenAI API
↑ ↓
└──────── 流式响应 ←────────────┘
```
## 配置
### 全局默认配置
```go
var DefaultSettings = map[string]string{
"llm.endpoint": "https://api.deepseek.com",
"llm.api_key": "",
"llm.model": "deepseek-v4-pro",
}
```
### 用户配置
每个用户可以独立配置 LLM 参数,存储在 `user_settings` 表中。
| 键名 | 说明 | 示例 |
|------|------|------|
| llm.endpoint | API 端点 | https://api.deepseek.com |
| llm.api_key | API 密钥 | sk-... |
| llm.model | 模型名称 | deepseek-v4-pro |
### 支持的 LLM 提供商
| 提供商 | 端点 | 模型 |
|--------|------|------|
| DeepSeek | https://api.deepseek.com | deepseek-v4-pro |
| OpenAI | https://api.openai.com | gpt-4, gpt-3.5-turbo |
| Azure OpenAI | 自定义 | 自定义 |
| 本地部署 | http://localhost:8000 | 自定义 |
## LLM 服务 (services/llm.go)
### 配置读取
```go
func GetLLMConfig(db *sql.DB, userID int64) (LLMConfig, error) {
config := LLMConfig{}
rows, err := db.Query("SELECT `key`, value FROM user_settings WHERE user_id = ? AND `key` IN ('llm.endpoint', 'llm.api_key', 'llm.model')", userID)
// ...
if config.APIKey == "" {
return config, fmt.Errorf("LLM API key not configured — please set it in the settings page")
}
if config.Model == "" {
config.Model = "deepseek-v4-pro"
}
return config, nil
}
```
### 流式调用
```go
func ChatStream(config LLMConfig, messages []goopenai.ChatCompletionMessage, callback StreamCallback) (string, error) {
clientConfig := goopenai.DefaultConfig(config.APIKey)
if config.Endpoint != "" {
clientConfig.BaseURL = config.Endpoint
}
client := goopenai.NewClientWithConfig(clientConfig)
ctx := context.Background()
stream, err := client.CreateChatCompletionStream(ctx, goopenai.ChatCompletionRequest{
Model: config.Model,
Messages: messages,
Stream: true,
})
// ...
var fullResponse strings.Builder
for {
response, err := stream.Recv()
if err == io.EOF {
break
}
// ...
if len(response.Choices) > 0 {
content := response.Choices[0].Delta.Content
if content != "" {
fullResponse.WriteString(content)
if callback != nil {
callback("content", map[string]interface{}{"content": content})
}
}
}
}
return fullResponse.String(), nil
}
```
### JSON 提取
```go
func extractJSON(s string) string {
// 尝试从 Markdown 代码块提取
if idx := strings.Index(s, "```json"); idx >= 0 {
start := idx + 7
if end := strings.Index(s[start:], "```"); end >= 0 {
return strings.TrimSpace(s[start : start+end])
}
}
// 查找第一个 { 或 [
startObj := strings.Index(s, "{")
startArr := strings.Index(s, "[")
// 找到匹配的闭合括号
// ...
return s[start:]
}
```
## PR 描述生成
### 提示模板
```markdown
你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。
## Commit 记录
- abc1234 feat: add new feature
- def5678 fix: bug fix
## 代码变更 (Diff)
diff --git a/main.go b/main.go
...
请直接输出 Markdown 格式的 PR 描述,包含以下部分:
# 标题
**类型**: feat|fix|refactor|docs|chore|style|test|perf
## 概述
一段话概述变更内容
## 详细说明
按模块分组的详细变更说明
## 影响范围
影响范围说明
格式要求:
- 引用文件名、函数名、变量名等代码标识时,必须用反引号包裹
- 直接写出实际的代码名称,不要用任何占位符替代
- 不要输出 JSON,直接输出 Markdown
```
### 生成流程
1. **读取 LLM 配置**: 从用户设置获取
2. **打开仓库**: 使用 go-git
3. **获取提交**: 过滤 base 和 head 之间的提交
4. **获取差异**: 生成统一差异
5. **构建提示**: 包含提交记录和差异
6. **调用 LLM**: 流式生成
7. **返回结果**: Markdown 格式的 PR 描述
### 差异截断
```go
// 截断 diff 如果太大(约 60k 字符以保持在 token 限制内)
if len(diff) > 60000 {
diff = diff[:60000] + "\n\n... [diff truncated due to size]"
}
```
## AI 代码审查
### 单文件审查提示
```markdown
你是一个资深代码审查专家。请审查以下代码变更,给出专业的 Review 意见。
## 文件: main.go
## 变更行数: +42 / -10
## Diff
diff --git a/main.go b/main.go
...
请按以下 JSON 格式输出审查意见(直接输出 JSON 数组,不要包含 markdown 代码块标记):
[
{
"severity": "critical 或 warning 或 info",
"description": "问题描述",
"suggestion": "建议的修改方案",
"code_example": "建议的代码(如有)"
}
]
严重程度说明:
- critical: 严重问题(安全漏洞、数据丢失风险、崩溃风险)
- warning: 建议改进(性能问题、代码规范、可维护性)
- info: 提示信息(最佳实践、可选优化)
如果代码没有问题,输出空数组 []。
请用中文回复。
```
### 汇总生成提示
```markdown
以下是多个文件的代码审查结果,请给出整体评估:
### main.go (42 行变更)
[warning] 变量未使用
[info] 可以使用更简洁的写法
### utils.go (10 行变更)
没有发现问题
请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
{
"score": 7,
"overall": "总体评价(2-3 句话)",
"findings": "按严重程度排序的主要发现汇总",
"recommendations": "改进建议,用换行分隔多条建议"
}
注意:所有字段必须是字符串类型,不要使用数组。请用中文回复。
```
### 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
- 信号量控制最大并发
- 线程安全的回调函数
## 流式响应处理
### 后端回调
```go
type StreamCallback func(event string, data interface{})
// 使用回调发送事件
callback("content", map[string]interface{}{"content": content})
callback("suggestion", map[string]interface{}{
"file": filename,
"severity": severity,
"content": content,
})
```
### 前端处理
```javascript
SSE.post(url, body, {
content: (data) => {
// 追加 Markdown 内容
appendToOutput(data.content);
},
suggestion: (data) => {
// 显示审查建议
showSuggestion(data.file, data.severity, data.content);
},
summary: (data) => {
// 显示汇总
showSummary(data);
}
});
```
## 错误处理
### API 密钥未配置
```go
if config.APIKey == "" {
return config, fmt.Errorf("LLM API key not configured — please set it in the settings page")
}
```
### API 调用失败
```go
stream, err := client.CreateChatCompletionStream(ctx, request)
if err != nil {
return "", fmt.Errorf("create stream: %w", err)
}
```
### 流式读取错误
```go
response, err := stream.Recv()
if err != nil {
return fullResponse.String(), fmt.Errorf("stream recv: %w", err)
}
```
### JSON 解析失败
```go
jsonStr := extractJSON(fullResponse)
var suggestions []ReviewSuggestion
if err := json.Unmarshal([]byte(jsonStr), &suggestions); err != nil {
// 尝试单个对象
var single ReviewSuggestion
if err2 := json.Unmarshal([]byte(jsonStr), &single); err2 == nil {
suggestions = []ReviewSuggestion{single}
} else {
// 使用原始响应作为描述
suggestions = []ReviewSuggestion{{
Severity: "info",
Description: fullResponse,
}}
}
}
```
## Token 管理
### 估算
- 1 个中文字符 ≈ 2 tokens
- 1 个英文单词 ≈ 1 token
- 代码行 ≈ 2-3 tokens
### 限制
- 输入: 通常 4k-128k tokens
- 输出: 通常 4k-8k tokens
### 优化
- 截断大型差异
- 只分析 Top-N 文件
- 压缩提示文本
## 模型选择建议
### DeepSeek
- 优势: 中文支持好,代码理解强
- 适用: 中文项目,代码审查
- 模型: deepseek-v4-pro
### OpenAI GPT-4
- 优势: 综合能力强
- 适用: 复杂项目,多语言
- 模型: gpt-4, gpt-4-turbo
### 本地部署
- 优势: 数据隐私,无 API 费用
- 适用: 敏感代码,离线环境
- 模型: CodeLlama, Qwen
## 成本优化
### 减少 Token 使用
- 截断大型差异
- 只分析关键文件
- 使用更小的模型
### 缓存
- 缓存相同输入的输出
- 避免重复分析
### 批量处理
- 合并多个小请求
- 减少 API 调用次数
## 监控
### 关键指标
- API 调用次数
- Token 使用量
- 响应时间
- 错误率
### 日志
```go
log.Printf("LLM call: model=%s, tokens=%d, duration=%v", model, tokens, duration)
```
## 安全
### API 密钥
- 不要在代码中硬编码
- 使用环境变量或配置文件
- 定期轮换密钥
### 数据隐私
- 敏感代码考虑本地部署
- 不要发送不必要的上下文
- 遵守数据合规要求
## 故障排查
### 常见问题
1. **API 密钥错误**
- 检查密钥是否正确
- 确认密钥是否有效
2. **连接超时**
- 检查网络连接
- 确认端点 URL 正确
3. **Token 超限**
- 减少输入大小
- 使用更大的上下文窗口
4. **JSON 解析失败**
- 检查提示格式
- 添加更明确的格式要求
### 调试命令
```bash
# 测试 API 连接
curl https://api.deepseek.com/v1/models \
-H "Authorization: Bearer sk-..."
# 测试流式响应
curl -N https://api.deepseek.com/v1/chat/completions \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-pro","messages":[{"role":"user","content":"Hello"}],"stream":true}'
```
+548
View File
@@ -0,0 +1,548 @@
# 部署运维
## 概述
PR-Helper 支持多种部署方式,包括直接运行、Docker 容器化和反向代理配置。
## 环境要求
### 系统要求
| 资源 | 最低要求 | 推荐 |
|------|----------|------|
| CPU | 1 核 | 2 核 |
| 内存 | 512MB | 2GB |
| 磁盘 | 1GB | 10GB+ |
| OS | Linux/macOS/Windows | Linux |
### 软件依赖
| 软件 | 版本 |
|------|------|
| Go | 1.21+ |
| MySQL | 5.7+ / 8.0+ |
| Git | 2.0+ |
## 快速开始
### 1. 克隆项目
```bash
git clone https://github.com/your-org/pr-helper.git
cd pr-helper
```
### 2. 配置环境
```bash
cp .env.example .env
# 编辑 .env 文件,设置 MySQL 连接信息
```
### 3. 构建
```bash
go build -o pr-helper .
```
### 4. 运行
```bash
./pr-helper
```
服务器将在 `http://localhost:8080` 启动。
## 环境变量
### 完整配置
```env
# 服务器
PORT=8080 # 监听端口
GIN_MODE=release # Gin 模式 (debug/release)
SESSION_SECRET= # 会话密钥(留空自动生成)
# 数据目录
DATA_DIR=data # 数据存储目录
# MySQL
MYSQL_HOST=127.0.0.1 # 数据库主机
MYSQL_PORT=3306 # 数据库端口
MYSQL_USER=root # 数据库用户
MYSQL_PASSWORD= # 数据库密码
MYSQL_DATABASE=pr_helper # 数据库名称
```
### 配置说明
| 变量 | 必需 | 默认值 | 说明 |
|------|------|--------|------|
| PORT | 否 | 8080 | HTTP 监听端口 |
| GIN_MODE | 否 | debug | Gin 运行模式 |
| SESSION_SECRET | 否 | (随机) | 会话加密密钥 |
| DATA_DIR | 否 | data | 数据存储路径 |
| MYSQL_HOST | 否 | 127.0.0.1 | MySQL 主机地址 |
| MYSQL_PORT | 否 | 3306 | MySQL 端口 |
| MYSQL_USER | 否 | root | MySQL 用户名 |
| MYSQL_PASSWORD | 否 | (空) | MySQL 密码 |
| MYSQL_DATABASE | 否 | pr_helper | MySQL 数据库名 |
## Docker 部署
### 使用 Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- MYSQL_HOST=db
- MYSQL_PORT=3306
- MYSQL_USER=root
- MYSQL_PASSWORD=pr_helper_pass
- MYSQL_DATABASE=pr_helper
- GIN_MODE=release
volumes:
- app-data:/app/data
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=pr_helper_pass
- MYSQL_DATABASE=pr_helper
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 5
volumes:
app-data:
mysql-data:
```
### 构建镜像
```bash
docker compose build
```
### 启动服务
```bash
docker compose up -d
```
### 查看日志
```bash
docker compose logs -f app
```
### 停止服务
```bash
docker compose down
```
## Dockerfile
```dockerfile
# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o pr-helper .
# 运行阶段
FROM alpine:3.18
RUN apk --no-cache add ca-certificates git
WORKDIR /app
COPY --from=builder /app/pr-helper .
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/static ./static
EXPOSE 8080
CMD ["./pr-helper"]
```
## 反向代理
### Nginx 配置
```nginx
server {
listen 80;
server_name pr-helper.example.com;
# SSE 支持
proxy_buffering off;
proxy_cache off;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持(如果需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# SSE 超时
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
```
### Caddy 配置
```
pr-helper.example.com {
reverse_proxy localhost:8080
}
```
## SSL/TLS
### Let's Encrypt
```bash
# 安装 certbot
sudo apt install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d pr-helper.example.com
# 自动续期
sudo certbot renew --dry-run
```
### 手动配置
```nginx
server {
listen 443 ssl;
server_name pr-helper.example.com;
ssl_certificate /etc/ssl/certs/pr-helper.crt;
ssl_certificate_key /etc/ssl/private/pr-helper.key;
# ... 其他配置
}
```
## 系统服务
### systemd 服务
```ini
# /etc/systemd/system/pr-helper.service
[Unit]
Description=PR-Helper Service
After=network.target mysql.service
[Service]
Type=simple
User=pr-helper
Group=pr-helper
WorkingDirectory=/opt/pr-helper
ExecStart=/opt/pr-helper/pr-helper
Restart=always
RestartSec=5
Environment=GIN_MODE=release
EnvironmentFile=/opt/pr-helper/.env
[Install]
WantedBy=multi-user.target
```
### 启用服务
```bash
sudo systemctl daemon-reload
sudo systemctl enable pr-helper
sudo systemctl start pr-helper
sudo systemctl status pr-helper
```
### 查看日志
```bash
sudo journalctl -u pr-helper -f
```
## 数据备份
### 数据库备份
```bash
# 备份
mysqldump -u root -p pr_helper > backup_$(date +%Y%m%d).sql
# 恢复
mysql -u root -p pr_helper < backup_20240101.sql
```
### 文件备份
```bash
# 备份数据目录
tar -czf data_backup_$(date +%Y%m%d).tar.gz data/
# 恢复
tar -xzf data_backup_20240101.tar.gz
```
### 自动备份脚本
```bash
#!/bin/bash
# backup.sh
BACKUP_DIR="/backup/pr-helper"
DATE=$(date +%Y%m%d)
# 创建备份目录
mkdir -p $BACKUP_DIR
# 备份数据库
mysqldump -u root -p pr_helper > $BACKUP_DIR/db_$DATE.sql
# 备份数据目录
tar -czf $BACKUP_DIR/data_$DATE.tar.gz data/
# 删除 7 天前的备份
find $BACKUP_DIR -type f -mtime +7 -delete
```
### 定时任务
```bash
# 每天凌晨 2 点备份
0 2 * * * /opt/pr-helper/backup.sh
```
## 监控
### 健康检查
```bash
curl http://localhost:8080/health
```
### 关键指标
- HTTP 请求速率
- 响应时间
- 错误率
- 内存使用
- CPU 使用
- 磁盘使用
### Prometheus 集成
```yaml
# prometheus.yml
scrape_configs:
- job_name: 'pr-helper'
static_configs:
- targets: ['localhost:8080']
```
### Grafana 仪表板
- HTTP 请求速率
- 响应时间分布
- 错误率趋势
- 系统资源使用
## 日志管理
### 日志级别
- debug: 调试信息
- info: 一般信息
- warn: 警告
- error: 错误
### 日志格式
```
2024/01/01 12:00:00 PR-Helper starting on :8080
2024/01/01 12:00:01 SSE connected: 192.168.1.100
2024/01/01 12:00:02 LLM call: model=deepseek-v4-pro, tokens=1500
```
### 日志轮转
```bash
# /etc/logrotate.d/pr-helper
/opt/pr-helper/logs/*.log {
daily
missingok
rotate 7
compress
delaycompress
notifempty
create 0644 pr-helper pr-helper
}
```
## 性能优化
### 数据库优化
```sql
-- 添加索引
CREATE INDEX idx_analyses_repo_id ON analyses(repo_id);
CREATE INDEX idx_analyses_user_id ON analyses(user_id);
CREATE INDEX idx_review_notes_analysis_id ON review_notes(analysis_id);
-- 优化查询
ANALYZE TABLE analyses;
ANALYZE TABLE repositories;
```
### 应用优化
- 启用 Gzip 压缩
- 使用 CDN 加速静态资源
- 配置合理的连接池大小
- 启用 HTTP/2
### 系统优化
```bash
# 增加文件描述符限制
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf
# 优化内核参数
echo "net.core.somaxconn = 65535" >> /etc/sysctl.conf
sysctl -p
```
## 安全加固
### 防火墙
```bash
# 只允许必要端口
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
### SELinux
```bash
# 允许网络连接
setsebool -P httpd_can_network_connect 1
```
### 安全头
```nginx
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-origin";
```
## 升级
### 停止服务
```bash
sudo systemctl stop pr-helper
```
### 备份
```bash
./backup.sh
```
### 更新代码
```bash
git pull origin main
go build -o pr-helper .
```
### 数据库迁移
```bash
# 自动迁移会在启动时执行
```
### 启动服务
```bash
sudo systemctl start pr-helper
```
### 验证
```bash
curl http://localhost:8080/
```
## 故障排查
### 常见问题
1. **数据库连接失败**
- 检查 MySQL 服务状态
- 验证连接参数
- 检查防火墙规则
2. **端口被占用**
```bash
lsof -i :8080
kill -9 <PID>
```
3. **权限问题**
```bash
chown -R pr-helper:pr-helper /opt/pr-helper
chmod 755 /opt/pr-helper/pr-helper
```
4. **内存不足**
- 增加系统内存
- 调整 MySQL 缓冲区大小
- 限制并发数
### 调试模式
```bash
GIN_MODE=debug ./pr-helper
```
### 查看日志
```bash
# systemd 日志
sudo journalctl -u pr-helper -f
# 应用日志
tail -f /var/log/pr-helper/app.log
```
+711
View File
@@ -0,0 +1,711 @@
# 开发指南
## 概述
本文档为 PR-Helper 开发者提供开发环境搭建、代码规范和贡献指南。
## 开发环境
### 系统要求
| 工具 | 版本 | 说明 |
|------|------|------|
| Go | 1.21+ | 后端语言 |
| MySQL | 5.7+ / 8.0+ | 数据库 |
| Git | 2.0+ | 版本控制 |
| Node.js | 18+ (可选) | 前端工具链 |
### IDE 推荐
- **GoLand**: JetBrains Go IDE
- **VS Code**: + Go 扩展
- **Vim/Neovim**: + vim-go
### 环境配置
```bash
# 安装 Go
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
# 安装 MySQL
sudo apt install mysql-server
sudo mysql_secure_installation
# 安装 Git
sudo apt install git
```
## 项目搭建
### 克隆项目
```bash
git clone https://github.com/your-org/pr-helper.git
cd pr-helper
```
### 安装依赖
```bash
go mod download
```
### 配置环境
```bash
cp .env.example .env
# 编辑 .env 文件
```
### 创建数据库
```sql
CREATE DATABASE pr_helper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
### 运行项目
```bash
go run .
```
## 项目结构
```
pr-helper/
├── main.go # 应用入口
├── go.mod # Go 模块定义
├── go.sum # 依赖校验
├── .env.example # 环境变量示例
├── Dockerfile # Docker 构建
├── docker-compose.yml # Docker Compose
├── config/ # 配置加载
│ └── config.go
├── database/ # 数据库操作
│ └── db.go
├── handlers/ # HTTP 处理器
│ ├── auth.go
│ ├── generate.go
│ ├── pages.go
│ ├── repos.go
│ ├── review.go
│ ├── settings.go
│ └── middleware.go
├── models/ # 数据模型
│ ├── repository.go
│ ├── analysis.go
│ ├── settings.go
│ └── user.go
├── services/ # 业务逻辑
│ ├── git.go
│ ├── llm.go
│ ├── generate.go
│ ├── review.go
│ ├── cache.go
│ └── notes.go
├── templates/ # HTML 模板
│ ├── layouts/
│ ├── pages/
│ └── partials/
├── static/ # 静态资源
│ ├── css/
│ ├── js/
│ └── lib/
└── docs/ # 文档
```
## 代码规范
### Go 代码规范
#### 命名规范
```go
// 包名: 小写单词
package services
// 结构体: 大驼峰
type ReviewResult struct {
FileReviews []FileReview
Summary ReviewSummary
}
// 函数: 大驼峰(导出)/ 小驼峰(未导出)
func GenerateReview(...) (*ReviewResult, error) {
// ...
}
func countDiffLines(patch string) int {
// ...
}
// 常量: 大驼峰或全大写
const MaxDiffSize = 60000
const MAX_FILES = 300
// 变量: 小驼峰
var defaultModel = "deepseek-v4-pro"
```
#### 注释规范
```go
// GenerateReview performs AI code review on diff files with Top-N strategy.
// It streams events (file_start, suggestion, file_end, summary, done) via callback
// and returns the complete ReviewResult for persistence.
func GenerateReview(db *sql.DB, repoPath, base, head string, topN, concurrency int, userID int64, callback StreamCallback) (*ReviewResult, error) {
// ...
}
```
#### 错误处理
```go
// 显式错误检查
result, err := doSomething()
if err != nil {
return fmt.Errorf("do something: %w", err)
}
// 忽略不需要的错误
db.conn.Exec(s) // 忽略错误(列已存在)
```
#### 表驱动测试
```go
func TestCountDiffLines(t *testing.T) {
tests := []struct {
name string
patch string
expected int
}{
{"empty", "", 0},
{"single add", "+line", 1},
{"single del", "-line", 1},
{"mixed", "+line1\n-line2", 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := countDiffLines(tt.patch)
if result != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, result)
}
})
}
}
```
### JavaScript 代码规范
#### 命名规范
```javascript
// 常量: 全大写
const MAX_FILES = 300;
const BRANCH_COLORS = ['#3b82f6', '#f97316', ...];
// 变量/函数: 小驼峰
let selectedBase = null;
function countDiffLines(patch) { ... }
// 类/对象: 大驼峰
const DiffViewer = { ... };
const GitGraph = { ... };
// 私有方法: 下划线前缀
_ensureFileIndexed(filename) { ... }
```
#### 注释规范
```javascript
/**
* POST to a streaming endpoint and handle SSE events.
* @param {string} url - The endpoint URL
* @param {object} body - JSON request body
* @param {object} handlers - Map of event name → callback(data)
* @returns {object} controller with abort() method
*/
async post(url, body, handlers = {}) {
// ...
}
```
### HTML 模板规范
```html
<!-- 使用语义化标签 -->
<header>...</header>
<main>...</main>
<footer>...</footer>
<!-- 使用 Tailwind CSS -->
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-xl font-semibold mb-4">标题</h2>
</div>
<!-- HTMX 属性 -->
<button hx-post="/api/repos/1/pull"
hx-trigger="click"
hx-indicator="#spinner">
拉取更新
</button>
```
## 添加新功能
### 1. 添加新 API 端点
#### 定义路由 (main.go)
```go
r.POST("/api/repos/:id/new-feature", authMw, handler.NewFeature)
```
#### 实现处理器 (handlers/new_feature.go)
```go
type NewFeatureHandler struct {
db *sql.DB
}
func NewNewFeatureHandler(db *sql.DB) *NewFeatureHandler {
return &NewFeatureHandler{db: db}
}
func (h *NewFeatureHandler) NewFeature(c *gin.Context) {
user := GetCurrentUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
// 解析请求
var req struct {
Param1 string `json:"param1" binding:"required"`
Param2 int `json:"param2"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 调用服务层
result, err := services.DoSomething(h.db, req.Param1, req.Param2)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回响应
c.JSON(http.StatusOK, result)
}
```
#### 实现服务层 (services/new_feature.go)
```go
func DoSomething(db *sql.DB, param1 string, param2 int) (*Result, error) {
// 业务逻辑
// ...
return result, nil
}
```
### 2. 添加 SSE 流式端点
#### 处理器
```go
func (h *Handler) StreamEndpoint(c *gin.Context) {
// 设置 SSE 头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "不支持流式传输"})
return
}
sendEvent := func(event string, data interface{}) {
jsonData, _ := json.Marshal(data)
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, jsonData)
flusher.Flush()
}
// 调用服务层
err := services.DoStream(h.db, sendEvent)
if err != nil {
sendEvent("error", map[string]interface{}{"message": err.Error()})
}
}
```
#### 前端调用
```javascript
SSE.post('/api/repos/1/stream', {}, {
progress: (data) => updateProgress(data),
done: () => hideSpinner(),
error: (data) => showError(data.message)
});
```
### 3. 添加新数据模型
#### 定义模型 (models/new_model.go)
```go
type NewModel struct {
ID int64 `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
```
#### 数据库迁移 (database/db.go)
```go
// 在 migrate() 函数中添加
"CREATE TABLE IF NOT EXISTS new_models (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY," +
"name VARCHAR(255) NOT NULL," +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
```
### 4. 添加新前端组件
#### 创建 JS 文件 (static/js/new_component.js)
```javascript
const NewComponent = {
init(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error('Container not found:', containerId);
return;
}
this.loadData();
},
async loadData() {
try {
const resp = await fetch('/api/data');
const data = await resp.json();
this.render(data);
} catch (err) {
this.container.innerHTML = `<div class="text-red-500">加载失败</div>`;
}
},
render(data) {
this.container.innerHTML = `
<div class="new-component">
${data.map(item => `<div>${item.name}</div>`).join('')}
</div>
`;
}
};
window.NewComponent = NewComponent;
```
#### 在模板中使用
```html
<script src="/static/js/new_component.js"></script>
<div id="new-component-container"></div>
<script>
NewComponent.init('new-component-container');
</script>
```
## 测试
### 单元测试
```go
// services/git_test.go
package services
import (
"testing"
)
func TestCountDiffLines(t *testing.T) {
tests := []struct {
name string
patch string
expected int
}{
{"empty", "", 0},
{"single add", "+line", 1},
{"single del", "-line", 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := countDiffLines(tt.patch)
if result != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, result)
}
})
}
}
```
### 集成测试
```go
// handlers/repos_test.go
package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestListRepos(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/repos", handler.ListRepos)
req, _ := http.NewRequest("GET", "/api/repos", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}
```
### 运行测试
```bash
# 运行所有测试
go test ./...
# 运行特定包的测试
go test ./services/...
# 运行特定测试
go test -run TestCountDiffLines ./services/
# 生成覆盖率报告
go test -cover ./...
```
## 调试
### Go 调试
```bash
# 使用 delve
dlv debug .
# 设置断点
(dlv) break main.main
(dlv) continue
(dlv) print cfg
```
### 日志调试
```go
import "log"
log.Printf("DEBUG: variable = %v", variable)
```
### 浏览器调试
- 打开开发者工具 (F12)
- Network 面板查看请求
- Console 面板查看日志
- Elements 面板查看 DOM
## Git 工作流
### 分支策略
```
main ← 生产分支
├── develop ← 开发分支
│ ├── feature/xxx ← 功能分支
│ └── fix/xxx ← 修复分支
└── release/x.x.x ← 发布分支
```
### 提交规范
```
<type>(<scope>): <subject>
<body>
<footer>
```
**类型**:
- `feat`: 新功能
- `fix`: 修复
- `docs`: 文档
- `style`: 格式
- `refactor`: 重构
- `test`: 测试
- `chore`: 构建/工具
**示例**:
```
feat(review): add Top-N strategy for large diffs
- Sort files by change size
- Analyze only top N files
- Generate summary from all results
Closes #123
```
### Pull Request
1. 从 develop 创建功能分支
2. 完成开发并测试
3. 提交 PR 到 develop
4. 代码审查
5. 合并并删除分支
## 发布流程
### 版本号
遵循语义化版本 (SemVer):
```
MAJOR.MINOR.PATCH
MAJOR: 不兼容的 API 变更
MINOR: 向后兼容的功能添加
PATCH: 向后兼容的修复
```
### 发布步骤
```bash
# 1. 更新版本号
# 编辑 main.go 或使用 git tag
# 2. 更新 CHANGELOG.md
# 3. 提交
git add .
git commit -m "release: v1.2.0"
# 4. 打标签
git tag -a v1.2.0 -m "Release v1.2.0"
# 5. 推送
git push origin main --tags
# 6. 构建 Docker 镜像
docker build -t pr-helper:v1.2.0 .
docker tag pr-helper:v1.2.0 pr-helper:latest
# 7. 推送镜像
docker push pr-helper:v1.2.0
docker push pr-helper:latest
```
## 文档
### 代码文档
- 使用 GoDoc 格式
- 包级别文档在 doc.go 中
- 导出函数必须有文档注释
### 用户文档
- README.md: 项目简介
- docs/: 详细文档
- CHANGELOG.md: 变更日志
### API 文档
- 使用 OpenAPI/Swagger (可选)
- 或在 docs/ 中手动维护
## 贡献指南
### 贡献流程
1. Fork 项目
2. 创建功能分支
3. 提交代码
4. 创建 Pull Request
5. 代码审查
6. 合并
### 代码审查清单
- [ ] 代码符合规范
- [ ] 测试通过
- [ ] 文档更新
- [ ] 无安全漏洞
- [ ] 性能可接受
### 报告问题
使用 GitHub Issues 报告问题,包含:
- 问题描述
- 复现步骤
- 期望行为
- 实际行为
- 环境信息
## 常见问题
### Q: 如何添加新的 LLM 提供商?
A: 实现 `services/llm.go` 中的 `ChatStream` 函数,支持不同的 API 格式。
### Q: 如何修改数据库表结构?
A: 在 `database/db.go` 的 `migrate()` 函数中添加 ALTER TABLE 语句。
### Q: 如何添加新的前端库?
A: 将库文件放入 `static/lib/` 目录,然后在模板中引入。
### Q: 如何调试 SSE 流?
A: 使用浏览器开发者工具的 Network 面板,查看 EventStream 请求。
## 资源
### 官方文档
- [Go 文档](https://go.dev/doc/)
- [Gin 文档](https://gin-gonic.com/docs/)
- [go-git 文档](https://pkg.go.dev/github.com/go-git/go-git/v5)
- [HTMX 文档](https://htmx.org/docs/)
- [D3.js 文档](https://d3js.org/)
### 社区
- [Go Forum](https://forum.golangbridge.org/)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/go)
- [GitHub Discussions](https://github.com/your-org/pr-helper/discussions)
+591
View File
@@ -0,0 +1,591 @@
# 故障排查
## 概述
本文档提供 PR-Helper 常见问题的诊断和解决方案。
## 快速诊断
### 健康检查
```bash
# 检查服务状态
curl http://localhost:8080/
# 检查数据库连接
curl http://localhost:8080/api/repos
```
### 查看日志
```bash
# systemd 日志
sudo journalctl -u pr-helper -f
# Docker 日志
docker compose logs -f app
# 直接运行时的输出
./pr-helper 2>&1 | tee app.log
```
## 常见问题
### 1. 服务无法启动
#### 端口被占用
**错误信息**:
```
listen tcp :8080: bind: address already in use
```
**解决方案**:
```bash
# 查找占用端口的进程
lsof -i :8080
# 终止进程
kill -9 <PID>
# 或使用其他端口
PORT=8081 ./pr-helper
```
#### 数据库连接失败
**错误信息**:
```
failed to initialize database: dial tcp 127.0.0.1:3306: connect: connection refused
```
**解决方案**:
```bash
# 检查 MySQL 服务状态
sudo systemctl status mysql
# 启动 MySQL
sudo systemctl start mysql
# 验证连接参数
mysql -u root -p -h 127.0.0.1 -P 3306 pr_helper
# 检查 .env 配置
cat .env | grep MYSQL
```
#### 数据库不存在
**错误信息**:
```
Unknown database 'pr_helper'
```
**解决方案**:
```sql
CREATE DATABASE pr_helper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
#### 权限问题
**错误信息**:
```
permission denied
```
**解决方案**:
```bash
# 检查文件权限
ls -la pr-helper
# 添加执行权限
chmod +x pr-helper
# 检查数据目录权限
ls -la data/
chmod -R 755 data/
```
### 2. 克隆仓库失败
#### 网络连接问题
**错误信息**:
```
dial tcp: lookup github.com: no such host
```
**解决方案**:
```bash
# 检查网络连接
ping github.com
# 检查 DNS
nslookup github.com
# 检查代理设置
echo $HTTP_PROXY
echo $HTTPS_PROXY
```
#### 认证失败
**错误信息**:
```
authentication required
```
**解决方案**:
- 确认仓库 URL 正确
- 检查认证类型(none/https)
- 验证用户名和密码
- 对于私有仓库,使用个人访问令牌
#### 磁盘空间不足
**错误信息**:
```
no space left on device
```
**解决方案**:
```bash
# 检查磁盘空间
df -h
# 清理旧仓库
curl -X DELETE http://localhost:8080/api/repos/1
# 或手动清理
rm -rf data/repos/*
```
### 3. PR 生成失败
#### LLM API 密钥未配置
**错误信息**:
```
LLM API key not configured — please set it in the settings page
```
**解决方案**:
1. 访问设置页面 `/settings`
2. 配置 LLM API 密钥
3. 保存设置
#### LLM API 调用失败
**错误信息**:
```
create stream: Post "https://api.deepseek.com/v1/chat/completions": dial tcp: lookup api.deepseek.com: no such host
```
**解决方案**:
```bash
# 检查网络连接
curl https://api.deepseek.com/v1/models
# 验证 API 密钥
curl https://api.deepseek.com/v1/models \
-H "Authorization: Bearer sk-..."
# 检查端点配置
# 访问 /settings 确认 llm.endpoint 正确
```
#### 差异过大
**错误信息**:
```
diff truncated due to size
```
**说明**:
- 差异超过 60k 字符会被截断
- 这是正常行为,不影响功能
**优化建议**:
- 减少变更范围
- 分多次提交
### 4. 代码审查失败
#### 文件数过多
**现象**:
- 审查时间过长
- 内存使用过高
**解决方案**:
- 调整 Top-N 设置(默认 20)
- 在请求中指定较小的 top_n 值
#### 并发数过高
**现象**:
- LLM API 限流
- 响应时间过长
**解决方案**:
- 降低并发数设置(默认 5)
- 在请求中指定较小的 concurrency 值
#### JSON 解析失败
**错误信息**:
```
invalid character '<' looking for beginning of value
```
**说明**:
- LLM 返回了非 JSON 格式的内容
- 系统会自动降级处理
**解决方案**:
- 检查 LLM 模型是否支持 JSON 输出
- 尝试使用其他模型
### 5. 前端显示异常
#### 样式丢失
**现象**:
- 页面布局混乱
- 样式不生效
**解决方案**:
```bash
# 重新编译 Tailwind CSS
npx tailwindcss -i ./static/css/input.css -o ./static/css/style.css --minify
# 清除浏览器缓存
# Chrome: Ctrl+Shift+Delete
```
#### JavaScript 错误
**现象**:
- 功能不工作
- 控制台报错
**解决方案**:
1. 打开浏览器开发者工具 (F12)
2. 查看 Console 面板的错误信息
3. 检查 Network 面板的请求状态
#### SSE 连接断开
**现象**:
- 流式输出中断
- 无响应
**解决方案**:
```bash
# 检查网络连接
ping localhost
# 检查防火墙
sudo ufw status
# 检查 Nginx 配置(如果有)
# 确保 proxy_buffering off;
```
### 6. 数据库问题
#### 连接数过多
**错误信息**:
```
too many connections
```
**解决方案**:
```sql
-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 增加最大连接数
SET GLOBAL max_connections = 200;
-- 或修改 my.cnf
-- max_connections = 200
```
#### 查询缓慢
**现象**:
- API 响应慢
- 页面加载超时
**解决方案**:
```sql
-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log';
SET GLOBAL slow_query_log = 'ON';
-- 分析查询
EXPLAIN SELECT * FROM analyses WHERE repo_id = 1;
-- 添加索引
CREATE INDEX idx_analyses_repo_id ON analyses(repo_id);
```
#### 数据损坏
**错误信息**:
```
Table 'pr_helper.analyses' doesn't exist
```
**解决方案**:
```bash
# 从备份恢复
mysql -u root -p pr_helper < backup.sql
# 或重新初始化
# 删除数据库并重启服务
```
### 7. Docker 问题
#### 容器无法启动
**错误信息**:
```
Error response from daemon: driver failed programming external connectivity
```
**解决方案**:
```bash
# 重启 Docker 服务
sudo systemctl restart docker
# 清理停止的容器
docker container prune
# 重新启动
docker compose up -d
```
#### 数据持久化问题
**现象**:
- 重启后数据丢失
**解决方案**:
```yaml
# 确保 docker-compose.yml 中配置了 volumes
services:
app:
volumes:
- app-data:/app/data
db:
volumes:
- mysql-data:/var/lib/mysql
volumes:
app-data:
mysql-data:
```
#### 网络问题
**现象**:
- 容器间无法通信
**解决方案**:
```bash
# 检查网络
docker network ls
docker network inspect pr-helper_default
# 重启网络
docker compose down
docker compose up -d
```
## 性能问题
### 1. 内存使用过高
**诊断**:
```bash
# 查看内存使用
free -h
top -o %MEM
# 查看进程内存
ps aux | grep pr-helper
```
**解决方案**:
- 减少并发数
- 限制 Top-N
- 增加系统内存
### 2. CPU 使用过高
**诊断**:
```bash
# 查看 CPU 使用
top -o %CPU
htop
```
**解决方案**:
- 减少并发数
- 优化数据库查询
- 使用更快的 LLM API
### 3. 磁盘 I/O 过高
**诊断**:
```bash
# 查看磁盘 I/O
iostat -x 1
iotop
```
**解决方案**:
- 使用 SSD
- 优化数据库索引
- 定期清理旧数据
## 安全问题
### 1. 未授权访问
**现象**:
- 陌生人访问了服务
**解决方案**:
- 配置防火墙
- 使用反向代理认证
- 不要暴露到公网
### 2. SQL 注入
**预防**:
- 使用参数化查询
- 验证输入数据
- 限制数据库权限
### 3. XSS 攻击
**预防**:
- 转义输出内容
- 使用 CSP 头
- 验证输入数据
## 调试工具
### 1. 日志级别
```bash
# 设置调试模式
GIN_MODE=debug ./pr-helper
# 查看详细日志
./pr-helper 2>&1 | tee debug.log
```
### 2. 数据库调试
```bash
# 连接数据库
mysql -u root -p pr_helper
# 查看表结构
DESCRIBE analyses;
# 查看数据
SELECT * FROM analyses LIMIT 10;
# 查看慢查询
SHOW PROCESSLIST;
```
### 3. 网络调试
```bash
# 测试 API
curl -v http://localhost:8080/api/repos
# 测试 SSE
curl -N http://localhost:8080/api/repos/1/review \
-H "Content-Type: application/json" \
-d '{"base":"main","head":"feature"}'
# 抓包
sudo tcpdump -i lo port 8080
```
### 4. 前端调试
```javascript
// 在浏览器控制台中
console.log('Debug info');
// 查看 SSE 连接
// Network 面板 → EventStream
// 查看 HTMX 请求
// Network 面板 → XHR
```
## 获取帮助
### 1. 收集信息
在报告问题时,请提供:
- 错误信息
- 复现步骤
- 环境信息(OS、Go 版本、MySQL 版本)
- 日志输出
- 配置文件(去除敏感信息)
### 2. 检查已知问题
```bash
# 查看 GitHub Issues
# https://github.com/your-org/pr-helper/issues
# 搜索类似问题
# 使用关键词搜索
```
### 3. 社区支持
- GitHub Issues: 报告 Bug
- GitHub Discussions: 提问和讨论
- Stack Overflow: 技术问题
## 预防措施
### 1. 定期备份
```bash
# 每日备份
0 2 * * * /opt/pr-helper/backup.sh
```
### 2. 监控告警
- 设置资源使用告警
- 监控错误率
- 监控响应时间
### 3. 更新维护
- 定期更新依赖
- 应用安全补丁
- 测试新版本
### 4. 文档记录
- 记录配置变更
- 记录故障处理
- 更新运维手册