refactor: replace chromedp with browser native print for PDF export

Remove chromedp dependency, use window.print() instead. Docker image no longer needs Chromium (~200MB smaller).

- Delete services/pdf.go and templates/reports/review.html

- Remove PDF API route POST /api/repos/:id/review/pdf

- Add @media print CSS to review page

- Remove chromium from Dockerfile
This commit is contained in:
2026-06-20 22:26:46 +08:00
parent 37aa9c0a33
commit 6c60b767df
10 changed files with 88 additions and 510 deletions
-222
View File
@@ -1,222 +0,0 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"os"
"time"
"github.com/HoHD/PR-Helper/models"
"github.com/chromedp/chromedp"
"github.com/chromedp/cdproto/page"
)
// ReportData holds all data needed to render the PDF report template.
type ReportData struct {
RepoURL string
BaseRef string
HeadRef string
ReviewedAt string
AnalysisID int64
Result string // raw JSON from analysis
Notes []models.ReviewNote
}
// ParsedReview holds the structured review data for the template.
type ParsedReview struct {
Score int
Overall string
Findings string
Recommendations string
FileReviews []FileReviewForReport
}
// FileReviewForReport is a file review entry formatted for the report template.
type FileReviewForReport struct {
FileName string
ChangeLines int
Suggestions []SuggestionForReport
Notes []string
}
// SuggestionForReport is a single suggestion formatted for the report template.
type SuggestionForReport struct {
Severity string
SeverityCN string
Description string
Suggestion string
CodeExample string
Notes []string
}
// parseReportData converts raw analysis JSON + notes into template-ready structures.
func parseReportData(data ReportData) ParsedReview {
result := ParsedReview{}
// Index notes by scope:scopeKey
noteMap := make(map[string][]string)
for _, n := range data.Notes {
key := n.Scope + ":" + n.ScopeKey
noteMap[key] = append(noteMap[key], n.Content)
}
// Try to parse structured analysis result (new ReviewResult format)
var reviewResult ReviewResult
if err := json.Unmarshal([]byte(data.Result), &reviewResult); err == nil && len(reviewResult.FileReviews) > 0 {
// New format: { file_reviews: [...], summary: {...}, top_n: N }
result.Score = reviewResult.Summary.Score
result.Overall = reviewResult.Summary.Overall
result.Findings = reviewResult.Summary.Findings
result.Recommendations = reviewResult.Summary.Recommendations
for _, fr := range reviewResult.FileReviews {
fileReport := FileReviewForReport{
FileName: fr.FileName,
ChangeLines: fr.ChangeLines,
}
// Collect file-level notes
fileReport.Notes = noteMap["file:"+fr.FileName]
for _, s := range fr.Suggestions {
sug := SuggestionForReport{
Severity: s.Severity,
SeverityCN: severityCN(s.Severity),
Description: s.Description,
Suggestion: s.Suggestion,
CodeExample: s.CodeExample,
}
fileReport.Suggestions = append(fileReport.Suggestions, sug)
}
result.FileReviews = append(result.FileReviews, fileReport)
}
return result
}
// Fallback: try legacy flat format { score, overall, findings, recommendations }
var analysisMap map[string]interface{}
if err := json.Unmarshal([]byte(data.Result), &analysisMap); err == nil {
if score, ok := analysisMap["score"].(float64); ok {
result.Score = int(score)
}
if overall, ok := analysisMap["overall"].(string); ok {
result.Overall = overall
}
if findings, ok := analysisMap["findings"].(string); ok {
result.Findings = findings
}
if recs, ok := analysisMap["recommendations"].(string); ok {
result.Recommendations = recs
}
}
return result
}
// severityCN returns the Chinese label for a severity level.
func severityCN(severity string) string {
switch severity {
case "critical":
return "严重"
case "warning":
return "建议"
case "info":
return "提示"
default:
return "提示"
}
}
// GeneratePDFReport generates a PDF from the review report data using chromedp.
func GeneratePDFReport(data ReportData) ([]byte, error) {
review := parseReportData(data)
// Collect overall notes
var overallNotes []string
for _, n := range data.Notes {
if n.Scope == "overall" {
overallNotes = append(overallNotes, n.Content)
}
}
// Build template data
tmplData := struct {
RepoURL string
BaseRef string
HeadRef string
ReviewedAt string
Score int
Overall string
Findings string
Recommendations string
FileReviews []FileReviewForReport
OverallNotes []string
Result string
}{
RepoURL: data.RepoURL,
BaseRef: data.BaseRef,
HeadRef: data.HeadRef,
ReviewedAt: data.ReviewedAt,
Score: review.Score,
Overall: review.Overall,
Findings: review.Findings,
Recommendations: review.Recommendations,
FileReviews: review.FileReviews,
OverallNotes: overallNotes,
Result: data.Result,
}
// Render HTML from template
tmpl, err := template.ParseFiles("templates/reports/review.html")
if err != nil {
return nil, fmt.Errorf("parse template: %w", err)
}
var htmlBuf bytes.Buffer
if err := tmpl.Execute(&htmlBuf, tmplData); err != nil {
return nil, fmt.Errorf("execute template: %w", err)
}
// Write HTML to temp file for chromedp
tmpFile, err := os.CreateTemp("", "pr-helper-report-*.html")
if err != nil {
return nil, fmt.Errorf("create temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.Write(htmlBuf.Bytes()); err != nil {
tmpFile.Close()
return nil, fmt.Errorf("write temp file: %w", err)
}
tmpFile.Close()
// Use chromedp to convert HTML to PDF
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var pdfBytes []byte
fileURL := "file://" + tmpFile.Name()
err = chromedp.Run(ctx,
chromedp.Navigate(fileURL),
chromedp.WaitReady("body"),
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
pdfBytes, _, err = page.PrintToPDF().
WithDisplayHeaderFooter(false).
WithPrintBackground(true).
Do(ctx)
return err
}),
)
if err != nil {
return nil, fmt.Errorf("chromedp: %w", err)
}
return pdfBytes, nil
}