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:
@@ -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
|
||||
|
||||
@@ -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 响应中
|
||||
@@ -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 超限
|
||||
|
||||
### 增量渲染
|
||||
|
||||
- 按文件分批
|
||||
- 异步处理
|
||||
- 进度反馈
|
||||
@@ -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
|
||||
@@ -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';
|
||||
```
|
||||
@@ -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
|
||||
|
||||
当前不支持跨域请求。仅限同源访问。
|
||||
@@ -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 抓包
|
||||
@@ -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}'
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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. 文档记录
|
||||
|
||||
- 记录配置变更
|
||||
- 记录故障处理
|
||||
- 更新运维手册
|
||||
Reference in New Issue
Block a user