From 145da12017857743f6f7afd67c0d6f30c23e9f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 13 May 2026 12:33:23 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 103 ++++++++++++++++++++++++++--------- web/static/js/auth.js | 21 +++++--- web/static/js/chat.js | 10 +++- web/static/js/monitor.js | 113 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 212 insertions(+), 35 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 7cfb045f..8a58241b 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3708,18 +3708,14 @@ header { .timeline-item-iteration { border-left-color: var(--accent-color); - background: rgba(0, 102, 255, 0.05); + background: rgba(0, 102, 255, 0.06); } -/* Eino 多代理:主编排器 vs 子代理时间线区分 */ -.timeline-eino-role-orchestrator { - border-left-color: #5c6bc0 !important; - background: rgba(92, 107, 192, 0.09) !important; -} -.timeline-eino-role-sub { - border-left-color: #00897b !important; - background: rgba(0, 137, 123, 0.08) !important; -} +/* + * Eino 主/子代理:保留 timeline-eino-role-* class(由 applyEinoTimelineRole 写入), + * 但不再在此处整卡铺色 + !important,否则会盖住工具调用/结果/思考的类型色。 + * 主编排 vs 子代理的区分由「迭代轮次」上的 timeline-eino-scope-* 负责。 + */ .timeline-item-iteration.timeline-eino-scope-main { border-left-color: #3949ab !important; background: rgba(57, 73, 171, 0.1) !important; @@ -3729,29 +3725,72 @@ header { background: rgba(0, 105, 92, 0.09) !important; } +/* 模型内部思考:弱化灰紫,避免与「助手输出」抢视觉 */ .timeline-item-thinking { - border-left-color: #9c27b0; - background: rgba(156, 39, 176, 0.05); + border-left-color: #7e57c2; + background: rgba(103, 58, 183, 0.06); +} + +/* 迭代中主通道流式正文(标题常为「助手输出」等):中性底 + 主色条,表示对用户可见的答复流 */ +.timeline-item-thinking[data-response-stream-placeholder="1"] { + border-left-color: var(--accent-color); + background: rgba(0, 102, 255, 0.04); } .timeline-item-reasoning_chain { - border-left-color: #5c6bc0; - background: rgba(92, 107, 192, 0.06); + border-left-color: #5e35b1; + background: rgba(94, 53, 177, 0.07); } +.timeline-item-planning { + border-left-color: #00838f; + background: rgba(0, 131, 143, 0.06); +} + +/* 工具调用:信息色(蓝),与「结果绿/红」分离;完成态不再用绿色以免与成功结果混淆 */ .timeline-item-tool_call { - border-left-color: #ff9800; - background: rgba(255, 152, 0, 0.05); + border-left-color: #1565c0; + background: rgba(21, 101, 192, 0.07); } .timeline-item-tool_result { + border-left-color: #78909c; + background: rgba(120, 144, 156, 0.06); +} + +.timeline-item-tool_result:has(.tool-result-section.success) { border-left-color: var(--success-color); - background: rgba(40, 167, 69, 0.05); + background: rgba(40, 167, 69, 0.07); +} + +.timeline-item-tool_result:has(.tool-result-section.error) { + border-left-color: var(--error-color); + background: rgba(220, 53, 69, 0.07); } .timeline-item-tool_result.error { border-left-color: var(--error-color); - background: rgba(220, 53, 69, 0.05); + background: rgba(220, 53, 69, 0.07); +} + +.timeline-item-eino_agent_reply { + border-left-color: #6a1b9a; + background: rgba(106, 27, 154, 0.07); +} + +.timeline-item-progress { + border-left-color: #607d8b; + background: rgba(96, 125, 139, 0.08); +} + +.timeline-item-warning { + border-left-color: #f57c00; + background: rgba(245, 124, 0, 0.09); +} + +.timeline-item-tool_calls_detected { + border-left-color: #0277bd; + background: rgba(2, 119, 189, 0.06); } .timeline-item-error { @@ -3941,20 +3980,36 @@ header { border: 1px solid rgba(220, 53, 69, 0.3); } -/* 工具调用项状态样式 */ +/* 工具调用项状态:全程保持「信息蓝」系,完成态不用绿色(避免与工具成功结果混淆) */ .timeline-item-tool_call.tool-call-running { - border-left-color: var(--accent-color); - background: rgba(0, 102, 255, 0.08); + border-left-color: #42a5f5; + background: rgba(66, 165, 245, 0.1); } .timeline-item-tool_call.tool-call-completed { - border-left-color: var(--success-color); - background: rgba(40, 167, 69, 0.08); + border-left-color: #0d47a1; + background: rgba(13, 71, 161, 0.08); } .timeline-item-tool_call.tool-call-failed { border-left-color: var(--error-color); - background: rgba(220, 53, 69, 0.08); + background: rgba(220, 53, 69, 0.1); +} + +/* 参数块与卡片类型色弱对齐,扫读时一眼归到「调用」 */ +.timeline-item-tool_call .tool-args { + background: rgba(21, 101, 192, 0.06); + border-color: rgba(21, 101, 192, 0.22); +} + +.timeline-item-tool_result:has(.tool-result-section.success) .tool-result { + background: rgba(40, 167, 69, 0.08); + border-color: rgba(40, 167, 69, 0.35); +} + +.timeline-item-tool_result:has(.tool-result-section.error) .tool-result { + background: rgba(220, 53, 69, 0.1); + border-color: rgba(220, 53, 69, 0.45); } /* 活跃任务栏 */ diff --git a/web/static/js/auth.js b/web/static/js/auth.js index f1a59adb..5511dfa0 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -342,22 +342,27 @@ function formatMarkdown(text) { ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'], ALLOW_DATA_ATTR: false, }; - + + const raw = text == null ? '' : String(text); + const src = typeof window.normalizeAssistantMarkdownSource === 'function' + ? window.normalizeAssistantMarkdownSource(raw) + : raw; + if (typeof DOMPurify !== 'undefined') { - if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(text)) { + if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(src)) { try { marked.setOptions({ breaks: true, gfm: true, }); - let parsedContent = marked.parse(text); + const parsedContent = marked.parse(src, { async: false }); return DOMPurify.sanitize(parsedContent, sanitizeConfig); } catch (e) { console.error('Markdown 解析失败:', e); - return DOMPurify.sanitize(text, sanitizeConfig); + return DOMPurify.sanitize(src, sanitizeConfig); } } else { - return DOMPurify.sanitize(text, sanitizeConfig); + return DOMPurify.sanitize(src, sanitizeConfig); } } else if (typeof marked !== 'undefined') { try { @@ -365,13 +370,13 @@ function formatMarkdown(text) { breaks: true, gfm: true, }); - return marked.parse(text); + return marked.parse(src, { async: false }); } catch (e) { console.error('Markdown 解析失败:', e); - return escapeHtml(text).replace(/\n/g, '
'); + return escapeHtml(src).replace(/\n/g, '
'); } } else { - return escapeHtml(text).replace(/\n/g, '
'); + return escapeHtml(src).replace(/\n/g, '
'); } } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 26fb02e1..ec2cbec2 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -1844,7 +1844,10 @@ function refreshSystemReadyMessageBubbles() { if (typeof marked !== 'undefined') { try { marked.setOptions({ breaks: true, gfm: true }); - const parsed = marked.parse(text); + const src = typeof window.normalizeAssistantMarkdownSource === 'function' + ? window.normalizeAssistantMarkdownSource(text) + : text; + const parsed = marked.parse(src, { async: false }); formattedContent = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(parsed, defaultSanitizeConfig) : parsed; @@ -1935,7 +1938,10 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr breaks: true, gfm: true, }); - return marked.parse(raw); + const src = typeof window.normalizeAssistantMarkdownSource === 'function' + ? window.normalizeAssistantMarkdownSource(raw) + : raw; + return marked.parse(src, { async: false }); } catch (e) { console.error('Markdown 解析失败:', e); return null; diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 17744afc..01967305 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -273,6 +273,116 @@ function escapeHtmlLocal(text) { return div.innerHTML; } +/** fenced 块占位(BMP 私用区,正文几乎不会出现) */ +const _MD_FENCE_PRE = '\n\uE000CSAI_FENCE_'; +const _MD_FENCE_SUF = '_\uE000\n'; + +function _maskFencedCodeBlocksForMdPreprocess(md) { + const blocks = []; + const masked = String(md).replace(/```[\s\S]*?```/g, (m) => { + const i = blocks.length; + blocks.push(m); + return _MD_FENCE_PRE + i + _MD_FENCE_SUF; + }); + return { masked, blocks }; +} + +function _unmaskFencedCodeBlocksAfterMdPreprocess(s, blocks) { + let out = s; + for (let i = 0; i < blocks.length; i++) { + out = out.split(_MD_FENCE_PRE + i + _MD_FENCE_SUF).join(blocks[i]); + } + return out; +} + +/** + * 模型/网关偶发把「思考」混进正文,用伪 XML 包裹(如 <redacted_thinking>…</redacted_thinking>)。 + * 与 Markdown 列表混排时,结束标签常被吞进 <li>,其后 **、` 等行内语法全部无法解析;成对块整段移除。 + * @param {string} segment + * @returns {string} + */ +function _stripXmlReasoningWrappersForMarkdown(segment) { + let t = String(segment); + const tags = ['redacted_thinking', 'redacted_reasoning']; + for (let i = 0; i < tags.length; i++) { + const name = tags[i]; + const re = new RegExp('<\\s*' + name + '\\b[^>]*>[\\s\\S]*?<\\s*/\\s*' + name + '\\s*>', 'gi'); + t = t.replace(re, '\n\n'); + } + return t.replace(/\n{3,}/g, '\n\n'); +} + +/** + * 解除 LLM 常用的块级 HTML 外壳(`
`、`

`、`

`、`
`、`
`)。 + * 整段包在块级标签里时,CommonMark 不会在块内再解析 Markdown,导致 **、` 原样显示。 + */ +function _unwrapHtmlBlockWrappersForMarkdown(segment) { + let s = segment; + let prev; + for (let i = 0; i < 30 && s !== prev; i++) { + prev = s; + s = s.replace(/]*)?>([\s\S]*?)<\/div>/gi, (_, inner) => String(inner).trim() + '\n\n'); + s = s.replace(/]*)?>([\s\S]*?)<\/p>/gi, (_, inner) => String(inner).trim() + '\n\n'); + s = s.replace(/]*)?>([\s\S]*?)<\/section>/gi, (_, inner) => String(inner).trim() + '\n\n'); + s = s.replace(/]*)?>([\s\S]*?)<\/article>/gi, (_, inner) => String(inner).trim() + '\n\n'); + s = s.replace(/]*)?>([\s\S]*?)<\/main>/gi, (_, inner) => String(inner).trim() + '\n\n'); + s = s.replace(/\n{3,}/g, '\n\n'); + } + return s; +} + +/** + * 将 HTML 列表 / 粘连的 `
  • ` 还原为 Markdown 列表行,并去掉外层 `
      `,便于 marked 解析行内 **、` ` + * @param {string} segment + * @returns {string} + */ +function _flattenOrphanHtmlLiInMarkdown(segment) { + let s = segment; + s = s.replace(/]*)?>([\s\S]*?)<\/li>/gi, (_, inner) => { + const body = String(inner).trim().replace(/\s*\n\s*/g, ' '); + return '- ' + body + '\n'; + }); + s = s.replace(/<\/?ul(?:\s[^>]*)?>/gi, '\n'); + s = s.replace(/<\/?ol(?:\s[^>]*)?>/gi, '\n'); + s = s.replace(/([0-9A-Za-z_\u4e00-\u9fff])\s*]*)?>\s*/g, (_, ch) => ch + '\n- '); + return s.replace(/\n{3,}/g, '\n\n'); +} + +/** 行首 Unicode 项目符号 → Markdown 列表 `- `(模型常用 • 而非 `-`) */ +function _normalizeUnicodeBulletMarkersToMdDash(segment) { + return segment + .replace(/^\s*\u2022\s+/gm, '- ') + .replace(/^\s*\u00b7\s+/gm, '- '); +} + +/** + * 解析前归一化助手 Markdown:去掉零宽字符,NFKC 将全角 * ` _ 等转为 ASCII, + * 避免 marked 无法识别强调/行内代码而原样显示 **、反引号; + * 并移除 <redacted_thinking> 等伪 XML 思考块、修正块级 HTML(`
      `/`

      `/…、`

        `/`
      • `)与 Unicode 项目符号 `•`,避免块级 HTML 吞掉 inline 解析。 + * @param {string|null|undefined} text + * @returns {string} + */ +function normalizeAssistantMarkdownSource(text) { + if (text == null) return ''; + let s = String(text); + s = s.replace(/[\u200B-\u200D\u200E\u200F\uFEFF\u2060]/g, ''); + try { + s = s.normalize('NFKC'); + } catch (e) { + /* ignore */ + } + s = _stripXmlReasoningWrappersForMarkdown(s); + const fb = _maskFencedCodeBlocksForMdPreprocess(s); + s = _unwrapHtmlBlockWrappersForMarkdown(fb.masked); + s = _flattenOrphanHtmlLiInMarkdown(s); + s = _normalizeUnicodeBulletMarkersToMdDash(s); + s = _unmaskFencedCodeBlocksAfterMdPreprocess(s, fb.blocks); + return s; +} +if (typeof window !== 'undefined') { + window.normalizeAssistantMarkdownSource = normalizeAssistantMarkdownSource; +} + /** * 与 internal/openai.normalizeStreamingDelta 一致:兼容网关/模型返回「累计全文」或整包重发, * 避免前端 buffer += chunk 与后端已归一化的增量叠加导致逐段重复(如「响应中显示了响应中显示了」)。 @@ -316,10 +426,11 @@ function setTimelineItemContentStreamRich(contentEl, html) { function formatAssistantMarkdownContent(text) { const raw = text == null ? '' : String(text); + const src = normalizeAssistantMarkdownSource(raw); if (typeof marked !== 'undefined') { try { marked.setOptions({ breaks: true, gfm: true }); - const parsed = marked.parse(raw); + const parsed = marked.parse(src, { async: false }); if (typeof DOMPurify !== 'undefined') { return DOMPurify.sanitize(parsed, assistantMarkdownSanitizeConfig); }