712 lines
14 KiB
Markdown
712 lines
14 KiB
Markdown
|
|
# 开发指南
|
||
|
|
|
||
|
|
## 概述
|
||
|
|
|
||
|
|
本文档为 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)
|