8e8ea6a07d
Deploy PR-Helper / deploy (push) Successful in 2m9s
- 新增 PR 描述 tab:流式生成 + 双栏实时预览(渲染效果 + Markdown 源码) - 新增 AI 代码审查 tab:Top-N/并发设置、历史记录、进度条、总结、备注、PDF 导出、内联 Diff - 保留 Diff 视图中的 AI 评审快捷内联按钮 - Repo handler 传递 TopN/Concurrency 设置 - 删除独立的 generate.html 和 review.html 页面及路由
153 lines
4.3 KiB
Go
153 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/HoHD/PR-Helper/config"
|
|
"github.com/HoHD/PR-Helper/database"
|
|
"github.com/HoHD/PR-Helper/handlers"
|
|
)
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
|
|
db, err := database.New(cfg.MySQLDSN())
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
gin.SetMode(cfg.GinMode)
|
|
r := gin.Default()
|
|
|
|
// Session middleware
|
|
store := cookie.NewStore([]byte(cfg.SessionSecret))
|
|
store.Options(sessions.Options{
|
|
MaxAge: 86400 * 7, // 7 days
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
})
|
|
r.Use(sessions.Sessions("pr_session", store))
|
|
|
|
// Custom template functions
|
|
funcMap := template.FuncMap{
|
|
"add": func(a, b int) int { return a + b },
|
|
"formatSize": func(bytes int64) string {
|
|
if bytes < 1024 {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
if bytes < 1024*1024 {
|
|
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
|
|
}
|
|
return fmt.Sprintf("%.1f MB", float64(bytes)/1024/1024)
|
|
},
|
|
}
|
|
|
|
// Load templates: base layout first, then pages and partials
|
|
tmpl := template.New("").Funcs(funcMap)
|
|
// Walk all template files and parse them together
|
|
filepath.Walk("templates", func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".html") {
|
|
return nil
|
|
}
|
|
data, readErr := os.ReadFile(path)
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
// Use the relative path as template name (e.g. "layouts/base.html")
|
|
name := strings.TrimPrefix(path, "templates/")
|
|
t, parseErr := tmpl.New(name).Parse(string(data))
|
|
if parseErr != nil {
|
|
log.Fatalf("failed to parse template %s: %v", name, parseErr)
|
|
}
|
|
tmpl = t
|
|
return nil
|
|
})
|
|
r.SetHTMLTemplate(tmpl)
|
|
|
|
// Static files
|
|
r.Static("/static", "./static")
|
|
|
|
// Handlers
|
|
auth := handlers.NewAuthHandler(db.Conn())
|
|
pages := handlers.NewPageHandler(db.Conn())
|
|
settings := handlers.NewSettingsHandler(db.Conn())
|
|
repos := handlers.NewReposHandler(db.Conn(), cfg.ReposDir())
|
|
generate := handlers.NewGenerateHandler(db.Conn())
|
|
review := handlers.NewReviewHandler(db.Conn())
|
|
|
|
// Public routes (no auth required)
|
|
r.GET("/login", auth.Login)
|
|
r.GET("/register", auth.Register)
|
|
r.POST("/api/auth/login", auth.HandleLogin)
|
|
r.POST("/api/auth/register", auth.HandleRegister)
|
|
r.POST("/api/auth/logout", auth.HandleLogout)
|
|
|
|
// Protected routes (auth required)
|
|
authMw := handlers.AuthRequired(db.Conn())
|
|
|
|
// Page routes
|
|
r.GET("/", authMw, pages.Index)
|
|
r.GET("/repo/:id", authMw, pages.Repo)
|
|
r.GET("/settings", authMw, pages.Settings)
|
|
|
|
// API routes
|
|
r.GET("/api/settings", authMw, settings.GetSettings)
|
|
r.PUT("/api/settings", authMw, settings.UpdateSettings)
|
|
r.GET("/api/repos", authMw, repos.ListRepos)
|
|
r.POST("/api/repos", authMw, repos.CloneRepo)
|
|
r.DELETE("/api/repos/:id", authMw, repos.DeleteRepo)
|
|
r.POST("/api/repos/:id/cleanup", authMw, repos.CleanupRepos)
|
|
r.POST("/api/repos/:id/pull", authMw, repos.PullRepo)
|
|
r.GET("/api/repos/:id/graph", authMw, repos.GetGraph)
|
|
r.GET("/api/repos/:id/refs", authMw, repos.GetRefs)
|
|
r.GET("/api/repos/:id/commits", authMw, repos.GetCommits)
|
|
r.GET("/api/repos/:id/diff", authMw, repos.GetDiff)
|
|
r.POST("/api/repos/:id/generate", authMw, generate.Generate)
|
|
r.POST("/api/repos/:id/review", authMw, review.Review)
|
|
r.GET("/api/repos/:id/review/analyses", authMw, review.ListReviews)
|
|
r.GET("/api/repos/:id/review/analyses/:aid", authMw, review.GetReview)
|
|
r.POST("/api/repos/:id/review/notes", authMw, review.SaveNotes)
|
|
r.GET("/api/repos/:id/review/notes", authMw, review.GetNotes)
|
|
|
|
|
|
// Graceful shutdown with signal handling
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.Port,
|
|
Handler: r,
|
|
}
|
|
|
|
go func() {
|
|
log.Printf("PR-Helper starting on :%s", cfg.Port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("failed to start server: %v", err)
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
log.Println("shutting down server...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Fatalf("server forced to shutdown: %v", err)
|
|
}
|
|
log.Println("server exited")
|
|
}
|