Files
PR-Helper/services/generate.go
T
wonder 496e3ba71e
Deploy PR-Helper / deploy (push) Successful in 30s
refactor: LLM 直接输出 Markdown,移除 JSON 解析层
服务端:
- prompt 改为要求直接输出 Markdown,不再输出 JSON
- GeneratePR 返回 string 而非 PRDescription 结构体
- 移除 JSON 解析、extractJSON、fixInlineCode 等逻辑
- ChatStream 的 content 事件直接携带 Markdown 片段流式传输

前端:
- generate() 仅处理 content + done + error 三个事件
- content 事件累积 Markdown 文本,实时渲染到两栏
- 移除 parsePartialJSON、buildMarkdown 等中间层
- 代码量从 ~100 行减至 ~50 行
2026-06-21 16:02:05 +08:00

134 lines
3.3 KiB
Go

package services
import (
"database/sql"
"fmt"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
goopenai "github.com/sashabaranov/go-openai"
)
// GeneratePR generates a PR description as Markdown from commit history and diff.
// It streams the LLM output via callback ("content" events) and returns the full markdown.
func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback StreamCallback) (string, error) {
// Read LLM config (per-user)
config, err := GetLLMConfig(db, userID)
if err != nil {
return "", err
}
// Open repo
repo, err := OpenRepo(repoPath)
if err != nil {
return "", fmt.Errorf("open repo: %w", err)
}
// Get commits between base and head
commits, err := GetCommitLog(repo, head, 100)
if err != nil {
return "", fmt.Errorf("get commits: %w", err)
}
// Filter commits to only those reachable from head but not from base
baseHash, err := repo.ResolveRevision(plumbing.Revision(base))
if err == nil {
baseCommit, bcErr := repo.CommitObject(*baseHash)
if bcErr == nil {
baseSet := make(map[string]bool)
baseQueue := []*object.Commit{baseCommit}
for len(baseQueue) > 0 {
c := baseQueue[0]
baseQueue = baseQueue[1:]
if baseSet[c.Hash.String()] {
continue
}
baseSet[c.Hash.String()] = true
for _, p := range c.ParentHashes {
pc, err := repo.CommitObject(p)
if err == nil {
baseQueue = append(baseQueue, pc)
}
}
}
var filtered []CommitInfo
for _, ci := range commits {
if !baseSet[ci.Hash] {
filtered = append(filtered, ci)
}
}
if len(filtered) > 0 {
commits = filtered
}
}
}
// Get diff
diff, err := GetDiff(repo, base, head)
if err != nil {
return "", fmt.Errorf("get diff: %w", err)
}
// Build prompt
var commitLines []string
for _, c := range commits {
commitLines = append(commitLines, fmt.Sprintf("- %s %s", c.ShortHash, c.Message))
}
commitStr := strings.Join(commitLines, "\n")
if commitStr == "" {
commitStr = "(no commits)"
}
// Truncate diff if too large (approx 60k chars to stay within token limits)
if len(diff) > 60000 {
diff = diff[:60000] + "\n\n... [diff truncated due to size]"
}
prompt := fmt.Sprintf(`你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。
## Commit 记录
%s
## 代码变更 (Diff)
%s
请直接输出 Markdown 格式的 PR 描述,包含以下部分:
# 标题
**类型**: feat|fix|refactor|docs|chore|style|test|perf
## 概述
一段话概述变更内容
## 详细说明
按模块分组的详细变更说明
## 影响范围
影响范围说明
格式要求:
- 引用文件名、函数名、变量名等代码标识时,必须用反引号包裹
- 直接写出实际的代码名称,不要用任何占位符替代
- 不要输出 JSON,直接输出 Markdown`, commitStr, diff)
messages := []goopenai.ChatCompletionMessage{
{Role: goopenai.ChatMessageRoleUser, Content: prompt},
}
// Call LLM with streaming — content events carry markdown chunks in real-time
fullResponse, err := ChatStream(config, messages, callback)
if err != nil {
return "", fmt.Errorf("LLM call: %w", err)
}
// Signal completion
if callback != nil {
callback("done", map[string]interface{}{"content": ""})
}
return fullResponse, nil
}