diff --git a/README.md b/README.md index 9c3fdbd..617a03d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/01-architecture.md b/docs/01-architecture.md new file mode 100644 index 0000000..03a32f0 --- /dev/null +++ b/docs/01-architecture.md @@ -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 响应中 diff --git a/docs/02-backend-services.md b/docs/02-backend-services.md new file mode 100644 index 0000000..7a5ac07 --- /dev/null +++ b/docs/02-backend-services.md @@ -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 超限 + +### 增量渲染 + +- 按文件分批 +- 异步处理 +- 进度反馈 diff --git a/docs/03-frontend-interaction.md b/docs/03-frontend-interaction.md new file mode 100644 index 0000000..9e1b0e3 --- /dev/null +++ b/docs/03-frontend-interaction.md @@ -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 +
+ +
+``` + +### 表单提交 + +```html +
+ + +
+``` + +### 事件触发 + +```html + +``` + +## 响应式设计 + +### 断点 + +- 移动端: < 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 diff --git a/docs/04-database-design.md b/docs/04-database-design.md new file mode 100644 index 0000000..2e6b418 --- /dev/null +++ b/docs/04-database-design.md @@ -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'; +``` diff --git a/docs/05-api-reference.md b/docs/05-api-reference.md new file mode 100644 index 0000000..4f95d28 --- /dev/null +++ b/docs/05-api-reference.md @@ -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 + +当前不支持跨域请求。仅限同源访问。 diff --git a/docs/06-sse-streaming.md b/docs/06-sse-streaming.md new file mode 100644 index 0000000..f9a8102 --- /dev/null +++ b/docs/06-sse-streaming.md @@ -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 抓包 diff --git a/docs/07-llm-integration.md b/docs/07-llm-integration.md new file mode 100644 index 0000000..d4535a0 --- /dev/null +++ b/docs/07-llm-integration.md @@ -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}' +``` diff --git a/docs/08-deployment.md b/docs/08-deployment.md new file mode 100644 index 0000000..35a442f --- /dev/null +++ b/docs/08-deployment.md @@ -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 + ``` + +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 +``` diff --git a/docs/09-development-guide.md b/docs/09-development-guide.md new file mode 100644 index 0000000..799c6c9 --- /dev/null +++ b/docs/09-development-guide.md @@ -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 + +
...
+
...
+ + + +
+

标题

+
+ + + +``` + +## 添加新功能 + +### 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 = `
加载失败
`; + } + }, + + render(data) { + this.container.innerHTML = ` +
+ ${data.map(item => `
${item.name}
`).join('')} +
+ `; + } +}; + +window.NewComponent = NewComponent; +``` + +#### 在模板中使用 + +```html + +
+ +``` + +## 测试 + +### 单元测试 + +```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 ← 发布分支 +``` + +### 提交规范 + +``` +(): + + + +