fix: 真正的流式输出 + 修复 Markdown 标题渲染
Deploy PR-Helper / deploy (push) Successful in 27s

流式输出:
- 恢复 content 事件处理,实时累积 LLM 原始输出
- parsePartialJSON 从不完整 JSON 中渐进提取已闭合的字段
- 每次提取到新字段即更新两栏,实现左栏渐进渲染
- 结构化事件(title/detail等)覆盖解析结果,确保最终一致

Markdown 渲染:
- 重写为逐行处理(_renderLines),不再按双换行分块
- ### 及更深层级标题在单换行后也能正确识别为独立块
- 列表项、水平线等同样受益于逐行解析
This commit is contained in:
2026-06-21 15:57:54 +08:00
parent cd1141d099
commit 779bd9e332
2 changed files with 94 additions and 42 deletions
+37 -7
View File
@@ -184,48 +184,77 @@
indicator.classList.remove('hidden');
// Clear previous results
document.getElementById('pr-rendered').innerHTML = '';
document.getElementById('pr-rendered').innerHTML = '<span class="animate-pulse text-gray-400">正在生成...</span>';
document.getElementById('pr-markdown').textContent = '';
markdownContent = '';
// Helper: update both columns from current fields
const fields = { title: '', type: '', summary: '', details: '', impact: '' };
let rawText = '';
let done = false;
function updateColumns() {
markdownContent = buildMarkdown(fields);
document.getElementById('pr-rendered').innerHTML = renderMarkdown(markdownContent);
document.getElementById('pr-markdown').textContent = markdownContent;
}
// Try to extract completed fields from partial JSON
function parsePartialJSON(text) {
const result = {};
for (const key of ['title', 'type', 'summary', 'details', 'impact']) {
// Match "key": "value" where value's closing quote exists
const re = new RegExp('"' + key + '"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"');
const m = text.match(re);
if (m) result[key] = m[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\\\/g, '\\');
}
return result;
}
SSE.post(`/api/repos/${repoId}/generate`, {
base: baseRef,
head: headRef,
}, {
content(data) {
if (done) return;
rawText += data.content || '';
// Parse partial JSON to extract fields progressively
const partial = parsePartialJSON(rawText);
let changed = false;
for (const key of ['title', 'type', 'summary', 'details', 'impact']) {
if (partial[key] && partial[key] !== fields[key]) {
fields[key] = partial[key];
changed = true;
}
}
if (changed) updateColumns();
},
title(data) {
fields.title += data.content || '';
fields.title = data.content || fields.title;
updateColumns();
document.getElementById('streaming-status').textContent = '解析标题...';
},
type(data) {
fields.type += data.content || '';
fields.type = data.content || fields.type;
updateColumns();
document.getElementById('streaming-status').textContent = '解析类型...';
},
summary(data) {
fields.summary += data.content || '';
fields.summary = data.content || fields.summary;
updateColumns();
document.getElementById('streaming-status').textContent = '解析摘要...';
},
detail(data) {
fields.details += data.content || '';
fields.details = data.content || fields.details;
updateColumns();
document.getElementById('streaming-status').textContent = '解析详细说明...';
},
impact(data) {
fields.impact += data.content || '';
fields.impact = data.content || fields.impact;
updateColumns();
document.getElementById('streaming-status').textContent = '解析影响范围...';
},
done() {
done = true;
updateColumns();
indicator.classList.add('hidden');
btn.disabled = false;
@@ -233,6 +262,7 @@
btn.classList.remove('opacity-70', 'cursor-not-allowed');
},
error(data) {
done = true;
showToast('生成失败: ' + (data.message || '未知错误'), 'error');
indicator.classList.add('hidden');
btn.disabled = false;