服务端: - prompt 改为要求直接输出 Markdown,不再输出 JSON - GeneratePR 返回 string 而非 PRDescription 结构体 - 移除 JSON 解析、extractJSON、fixInlineCode 等逻辑 - ChatStream 的 content 事件直接携带 Markdown 片段流式传输 前端: - generate() 仅处理 content + done + error 三个事件 - content 事件累积 Markdown 文本,实时渲染到两栏 - 移除 parsePartialJSON、buildMarkdown 等中间层 - 代码量从 ~100 行减至 ~50 行
This commit is contained in:
+30
-66
@@ -2,9 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
@@ -12,34 +10,25 @@ import (
|
||||
goopenai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// PRDescription is the structured output from PR description generation.
|
||||
type PRDescription struct {
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Summary string `json:"summary"`
|
||||
Details string `json:"details"`
|
||||
Impact string `json:"impact"`
|
||||
}
|
||||
|
||||
// GeneratePR generates a structured PR description from commit history and diff.
|
||||
// It streams progress via the callback and returns the parsed PR description.
|
||||
func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback StreamCallback) (*PRDescription, error) {
|
||||
// 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 nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Open repo
|
||||
repo, err := OpenRepo(repoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open repo: %w", err)
|
||||
return "", fmt.Errorf("open repo: %w", err)
|
||||
}
|
||||
|
||||
// Get commits between base and head
|
||||
commits, err := GetCommitLog(repo, head, 100)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get commits: %w", err)
|
||||
return "", fmt.Errorf("get commits: %w", err)
|
||||
}
|
||||
|
||||
// Filter commits to only those reachable from head but not from base
|
||||
@@ -78,7 +67,7 @@ func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback
|
||||
// Get diff
|
||||
diff, err := GetDiff(repo, base, head)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get diff: %w", err)
|
||||
return "", fmt.Errorf("get diff: %w", err)
|
||||
}
|
||||
|
||||
// Build prompt
|
||||
@@ -97,7 +86,7 @@ func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback
|
||||
diff = diff[:60000] + "\n\n... [diff truncated due to size]"
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份结构化的 PR 描述。
|
||||
prompt := fmt.Sprintf(`你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。
|
||||
|
||||
## Commit 记录
|
||||
%s
|
||||
@@ -105,65 +94,40 @@ func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback
|
||||
## 代码变更 (Diff)
|
||||
%s
|
||||
|
||||
请按以下 JSON 格式输出(直接输出 JSON,不要包含 markdown 代码块标记):
|
||||
{
|
||||
"title": "简洁的 PR 标题",
|
||||
"type": "变更类型: feat|fix|refactor|docs|chore|style|test|perf",
|
||||
"summary": "一段话概述变更内容",
|
||||
"details": "详细的变更说明,按模块分组,使用 Markdown 格式",
|
||||
"impact": "影响范围说明"
|
||||
}
|
||||
请直接输出 Markdown 格式的 PR 描述,包含以下部分:
|
||||
|
||||
重要格式要求:
|
||||
- details 字段中引用文件名、函数名、变量名等代码标识时,必须用反引号包裹,例如:`+"`services/generate.go`"+`, `+"`GeneratePR()`"+`
|
||||
- 直接写出实际的代码名称,不要用任何占位符替代`, commitStr, diff)
|
||||
# 标题
|
||||
|
||||
**类型**: feat|fix|refactor|docs|chore|style|test|perf
|
||||
|
||||
## 概述
|
||||
一段话概述变更内容
|
||||
|
||||
## 详细说明
|
||||
按模块分组的详细变更说明
|
||||
|
||||
## 影响范围
|
||||
影响范围说明
|
||||
|
||||
格式要求:
|
||||
- 引用文件名、函数名、变量名等代码标识时,必须用反引号包裹
|
||||
- 直接写出实际的代码名称,不要用任何占位符替代
|
||||
- 不要输出 JSON,直接输出 Markdown`, commitStr, diff)
|
||||
|
||||
messages := []goopenai.ChatCompletionMessage{
|
||||
{Role: goopenai.ChatMessageRoleUser, Content: prompt},
|
||||
}
|
||||
|
||||
// Call LLM with streaming
|
||||
// Call LLM with streaming — content events carry markdown chunks in real-time
|
||||
fullResponse, err := ChatStream(config, messages, callback)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM call: %w", err)
|
||||
return "", fmt.Errorf("LLM call: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
jsonStr := extractJSON(fullResponse)
|
||||
var pr PRDescription
|
||||
if err := json.Unmarshal([]byte(jsonStr), &pr); err != nil {
|
||||
return nil, fmt.Errorf("parse PR description: %w (raw: %s)", err, truncateString(fullResponse, 200))
|
||||
}
|
||||
|
||||
// Post-process: convert INLINECODE placeholders back to backtick-enclosed code
|
||||
pr.Details = fixInlineCode(pr.Details)
|
||||
|
||||
// Send structured events
|
||||
// Signal completion
|
||||
if callback != nil {
|
||||
callback("title", map[string]interface{}{"content": pr.Title})
|
||||
callback("type", map[string]interface{}{"content": pr.Type})
|
||||
callback("summary", map[string]interface{}{"content": pr.Summary})
|
||||
callback("detail", map[string]interface{}{"content": pr.Details})
|
||||
callback("impact", map[string]interface{}{"content": pr.Impact})
|
||||
callback("done", map[string]interface{}{"content": ""})
|
||||
}
|
||||
|
||||
return &pr, nil
|
||||
}
|
||||
|
||||
// fixInlineCode converts LLM-generated INLINECODE placeholders back to backtick-enclosed inline code.
|
||||
// Some LLMs output INLINECODE0, INLINECODE1, etc. instead of `code` in JSON string values.
|
||||
// This wraps them in backticks so the frontend markdown renderer displays them as inline code.
|
||||
func fixInlineCode(text string) string {
|
||||
re := regexp.MustCompile(`(?i)INLINECODE[_]?(\d+)`)
|
||||
return re.ReplaceAllStringFunc(text, func(match string) string {
|
||||
return "`" + match + "`"
|
||||
})
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
return fullResponse, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user