feat: SSE Last-Event-ID 断线重放 + GUI 表单防刷新

- webui handler: 新增 sseEventRing 环状缓冲区(200条),断线重连按 Last-Event-ID 重放遗漏事件
- GUI app.js: doRenderAll 检测连接表单打开时改走 refreshDataOnly,修复 15s 定时器擦掉用户输入的 bug
- 附带 dashboard.html/index.html 前端调整 + handler_sse_test.go 单测
This commit is contained in:
JianFeeeee
2026-08-24 16:22:28 +08:00
parent fb8a1b8ce3
commit 5163ce51a7
5 changed files with 365 additions and 24 deletions

View File

@ -17,6 +17,10 @@
src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"
onerror="console.warn('marked CDN failed')"
></script>
<script
src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"
onerror="console.warn('DOMPurify CDN failed')"
></script>
<script>
setTimeout(function () {
if (!window.THREE) window._THREE_FAILED = true;
@ -2508,6 +2512,25 @@ background:
})();
// ===== Utility =====
function renderMd(text) {
if (typeof text !== "string") text = String(text || "");
var html;
if (typeof marked !== "undefined") {
try { html = marked.parse(text); }
catch (e) { html = escHtml(text); }
} else {
html = "<pre>" + escHtml(text) + "</pre>";
}
if (typeof DOMPurify !== "undefined" && typeof DOMPurify.sanitize === "function") {
try { return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }); }
catch (e) {}
}
return html
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/\son\w+\s*=\s*"[^"]*"/gi, "")
.replace(/\son\w+\s*=\s*'[^']*'/gi, "")
.replace(/javascript:/gi, "");
}
function escHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
@ -3200,11 +3223,7 @@ background:
var role = m.role || "user";
var c = m.content || "";
if (role === "assistant") {
if (typeof marked !== "undefined") {
c = marked.parse(c);
} else {
c = "<pre>" + escHtml(c) + "</pre>";
}
c = renderMd(c);
} else if (role === "system") {
c = escHtml(c);
} else {
@ -3384,10 +3403,7 @@ background:
}
function renderReasoningCard(text, isStreaming) {
var body =
typeof marked !== "undefined"
? marked.parse(text)
: escHtml(text);
var body = renderMd(text);
var preview =
typeof marked !== "undefined"
? text.replace(/[\s\n]+/g, " ").slice(0, 60)