diff --git a/services/generate.go b/services/generate.go index d597edc..56a15f8 100644 --- a/services/generate.go +++ b/services/generate.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "fmt" + "regexp" "strings" "github.com/go-git/go-git/v5/plumbing" @@ -111,7 +112,11 @@ func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback "summary": "一段话概述变更内容", "details": "详细的变更说明,按模块分组,使用 Markdown 格式", "impact": "影响范围说明" -}`, commitStr, diff) +} + +重要格式要求: +- details 字段中引用文件名、函数名、变量名等代码标识时,必须用反引号包裹,例如:`+"`services/generate.go`"+`, `+"`GeneratePR()`"+` +- 直接写出实际的代码名称,不要用任何占位符替代`, commitStr, diff) messages := []goopenai.ChatCompletionMessage{ {Role: goopenai.ChatMessageRoleUser, Content: prompt}, @@ -130,6 +135,9 @@ func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback 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 if callback != nil { callback("title", map[string]interface{}{"content": pr.Title}) @@ -143,6 +151,16 @@ func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback 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 diff --git a/templates/pages/generate.html b/templates/pages/generate.html index c930778..393276c 100644 --- a/templates/pages/generate.html +++ b/templates/pages/generate.html @@ -67,7 +67,7 @@

-
+ @@ -213,6 +213,7 @@ document.getElementById('pr-details').innerHTML = ''; document.getElementById('pr-impact').textContent = ''; document.getElementById('pr-markdown').textContent = ''; + document.getElementById('markdown-preview').classList.add('hidden'); markdownContent = ''; // Accumulate streaming content @@ -252,6 +253,10 @@ document.getElementById('pr-markdown').textContent = markdownContent; }, done() { + // Assemble full markdown from structured fields for copy + markdownContent = buildMarkdown(fields); + document.getElementById('pr-markdown').textContent = markdownContent; + document.getElementById('markdown-preview').classList.remove('hidden'); streaming.classList.add('hidden'); outputContent.classList.remove('hidden'); btn.disabled = false; @@ -272,18 +277,39 @@ return Markdown.render(text); } + function buildMarkdown(f) { + const parts = []; + if (f.title) parts.push(`# ${f.title}`); + if (f.type) parts.push(`**类型**: ${f.type}`); + if (f.summary) parts.push(`## 摘要\n${f.summary}`); + if (f.details) parts.push(`## 详细说明\n${f.details}`); + if (f.impact) parts.push(`## 影响\n${f.impact}`); + return parts.join('\n\n'); + } + function copyMarkdown() { const markdown = markdownContent || document.getElementById('pr-markdown').textContent; + if (!markdown.trim()) { + showToast('没有可复制的内容,请先生成 PR 描述', 'warning'); + return; + } navigator.clipboard.writeText(markdown).then(() => { showToast('已复制到剪贴板', 'success'); }).catch(() => { + // Fallback for non-HTTPS or permission-denied contexts const textarea = document.createElement('textarea'); textarea.value = markdown; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); - document.execCommand('copy'); + try { + document.execCommand('copy'); + showToast('已复制到剪贴板', 'success'); + } catch (e) { + showToast('复制失败,请手动选择文本复制', 'error'); + } document.body.removeChild(textarea); - showToast('已复制到剪贴板', 'success'); }); }