- 改进 LLM 提示词,明确要求用反引号包裹代码引用,避免输出 INLINECODE 占位符 - 后端新增 fixInlineCode 后处理,将残留 INLINECODE 占位符转为反引号代码 - 前端在 done 回调中从结构化字段组装完整 markdown 文本,修复复制按钮复制空内容 - 复制按钮增加空内容检查和错误提示 - Markdown 预览区域初始隐藏,生成完成后自动显示
This commit is contained in:
+19
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"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": "一段话概述变更内容",
|
"summary": "一段话概述变更内容",
|
||||||
"details": "详细的变更说明,按模块分组,使用 Markdown 格式",
|
"details": "详细的变更说明,按模块分组,使用 Markdown 格式",
|
||||||
"impact": "影响范围说明"
|
"impact": "影响范围说明"
|
||||||
}`, commitStr, diff)
|
}
|
||||||
|
|
||||||
|
重要格式要求:
|
||||||
|
- details 字段中引用文件名、函数名、变量名等代码标识时,必须用反引号包裹,例如:`+"`services/generate.go`"+`, `+"`GeneratePR()`"+`
|
||||||
|
- 直接写出实际的代码名称,不要用任何占位符替代`, commitStr, diff)
|
||||||
|
|
||||||
messages := []goopenai.ChatCompletionMessage{
|
messages := []goopenai.ChatCompletionMessage{
|
||||||
{Role: goopenai.ChatMessageRoleUser, Content: prompt},
|
{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))
|
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
|
// Send structured events
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback("title", map[string]interface{}{"content": pr.Title})
|
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
|
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 {
|
func truncateString(s string, maxLen int) string {
|
||||||
if len(s) <= maxLen {
|
if len(s) <= maxLen {
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<p id="pr-impact" class="text-gray-700"></p>
|
<p id="pr-impact" class="text-gray-700"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 p-4 bg-gray-50 rounded-md">
|
<div id="markdown-preview" class="mt-6 p-4 bg-gray-50 rounded-md hidden">
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-2">Markdown 预览</label>
|
<label class="block text-sm font-medium text-gray-500 mb-2">Markdown 预览</label>
|
||||||
<pre id="pr-markdown" class="whitespace-pre-wrap text-sm text-gray-800 font-mono"></pre>
|
<pre id="pr-markdown" class="whitespace-pre-wrap text-sm text-gray-800 font-mono"></pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,6 +213,7 @@
|
|||||||
document.getElementById('pr-details').innerHTML = '';
|
document.getElementById('pr-details').innerHTML = '';
|
||||||
document.getElementById('pr-impact').textContent = '';
|
document.getElementById('pr-impact').textContent = '';
|
||||||
document.getElementById('pr-markdown').textContent = '';
|
document.getElementById('pr-markdown').textContent = '';
|
||||||
|
document.getElementById('markdown-preview').classList.add('hidden');
|
||||||
markdownContent = '';
|
markdownContent = '';
|
||||||
|
|
||||||
// Accumulate streaming content
|
// Accumulate streaming content
|
||||||
@@ -252,6 +253,10 @@
|
|||||||
document.getElementById('pr-markdown').textContent = markdownContent;
|
document.getElementById('pr-markdown').textContent = markdownContent;
|
||||||
},
|
},
|
||||||
done() {
|
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');
|
streaming.classList.add('hidden');
|
||||||
outputContent.classList.remove('hidden');
|
outputContent.classList.remove('hidden');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -272,18 +277,39 @@
|
|||||||
return Markdown.render(text);
|
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() {
|
function copyMarkdown() {
|
||||||
const markdown = markdownContent || document.getElementById('pr-markdown').textContent;
|
const markdown = markdownContent || document.getElementById('pr-markdown').textContent;
|
||||||
|
if (!markdown.trim()) {
|
||||||
|
showToast('没有可复制的内容,请先生成 PR 描述', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigator.clipboard.writeText(markdown).then(() => {
|
navigator.clipboard.writeText(markdown).then(() => {
|
||||||
showToast('已复制到剪贴板', 'success');
|
showToast('已复制到剪贴板', 'success');
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
// Fallback for non-HTTPS or permission-denied contexts
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
textarea.value = markdown;
|
textarea.value = markdown;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
document.body.appendChild(textarea);
|
document.body.appendChild(textarea);
|
||||||
textarea.select();
|
textarea.select();
|
||||||
document.execCommand('copy');
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
showToast('已复制到剪贴板', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
showToast('复制失败,请手动选择文本复制', 'error');
|
||||||
|
}
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
showToast('已复制到剪贴板', 'success');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user