From 84f365dba3ff9eb5cb74a42e97f8a37f3151c0a5 Mon Sep 17 00:00:00 2001 From: XianYuDaXian Date: Thu, 5 Mar 2026 23:40:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=80=9D=E8=80=83?= =?UTF-8?q?=E6=B5=81=E7=A8=B3=E5=AE=9A=E6=80=A7=E4=B8=8E=20agent=20?= =?UTF-8?q?=E5=8C=BA=E5=9D=97=E4=BA=A4=E4=BA=92=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/grok/services/chat.py | 38 ++++++++++--- app/static/public/css/chat.css | 65 +++++++++++++++++++--- app/static/public/js/chat.js | 89 +++++++++++++++++------------- 3 files changed, 139 insertions(+), 53 deletions(-) diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index 0ee6428..dae6ecc 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -419,6 +419,9 @@ class StreamProcessor(proc_base.BaseProcessor): self.rollout_id: str = "" self.fingerprint: str = "" self.think_opened: bool = False + self.seen_think_once: bool = False + self.image_think_active: bool = False + self.answer_buffer: list[str] = [] self.role_sent: bool = False self.filter_tags = get_config("app.filter_tags") self.tool_usage_enabled = ( @@ -566,12 +569,11 @@ class StreamProcessor(proc_base.BaseProcessor): if img := resp.get("streamingImageGenerationResponse"): if not self.show_think: continue - if is_thinking and not self.think_opened: + self.image_think_active = True + if not self.think_opened: yield self._sse("\n") self.think_opened = True - if (not is_thinking) and self.think_opened: - yield self._sse("\n\n") - self.think_opened = False + self.seen_think_once = True idx = img.get("imageIndex", 0) + 1 progress = img.get("progress", 0) yield self._sse( @@ -580,6 +582,7 @@ class StreamProcessor(proc_base.BaseProcessor): continue if mr := resp.get("modelResponse"): + self.image_think_active = False for url in proc_base._collect_images(mr): parts = url.split("/") img_id = parts[-2] if len(parts) >= 2 else "image" @@ -622,20 +625,37 @@ class StreamProcessor(proc_base.BaseProcessor): filtered = self._filter_token(token) if not filtered: continue - if is_thinking: + # 避免上游字面 标签与前端思考分段逻辑冲突,导致多段思考块 + filtered = re.sub(r"", "", filtered, flags=re.IGNORECASE) + if not filtered: + continue + in_think = ( + is_thinking + or self.image_think_active + ) + if in_think: if not self.show_think: continue if not self.think_opened: yield self._sse("\n") self.think_opened = True - else: - if self.think_opened: - yield self._sse("\n\n") - self.think_opened = False + self.seen_think_once = True + yield self._sse(filtered) + continue + + # 一旦进入思考模式,先缓存正文,等思考结束后再统一输出 + if self.show_think and self.seen_think_once: + self.answer_buffer.append(filtered) + continue + yield self._sse(filtered) if self.think_opened: yield self._sse("\n") + self.think_opened = False + if self.answer_buffer: + yield self._sse("".join(self.answer_buffer)) + self.answer_buffer = [] yield self._sse(finish="stop") yield "data: [DONE]\n\n" except asyncio.CancelledError: diff --git a/app/static/public/css/chat.css b/app/static/public/css/chat.css index a699a53..275f38b 100644 --- a/app/static/public/css/chat.css +++ b/app/static/public/css/chat.css @@ -484,7 +484,10 @@ body { } .think-agent { + border: 1px solid var(--chat-border); + background: var(--chat-surface); border-radius: 12px; + padding: 8px 10px 10px; width: 100%; } @@ -499,6 +502,9 @@ body { gap: 8px; padding: 4px 8px; border-radius: 999px; + background: transparent; + width: auto; + max-width: 100%; } .think-agent summary::before { @@ -518,7 +524,7 @@ body { border-right: 2px solid var(--accents-4); border-bottom: 2px solid var(--accents-4); transform: rotate(45deg); - margin-left: 4px; + margin-left: 2px; } .think-agent[open] summary::after { @@ -531,15 +537,15 @@ body { @keyframes thinkGlow { 0% { - box-shadow: 0 0 0 rgba(255, 255, 255, 0); + box-shadow: 0 0 0 rgba(17, 17, 17, 0); background: transparent; } 50% { - box-shadow: 0 0 12px rgba(255, 255, 255, 0.9); - background: rgba(255, 255, 255, 0.8); + box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.14); + background: rgba(14, 165, 233, 0.08); } 100% { - box-shadow: 0 0 0 rgba(255, 255, 255, 0); + box-shadow: 0 0 0 rgba(17, 17, 17, 0); background: transparent; } } @@ -551,7 +557,8 @@ body { .think-agent-items { margin-top: 10px; margin-left: 0; - padding-left: 0; + padding-left: 2px; + padding-right: 2px; border-left: none; display: flex; flex-direction: column; @@ -623,7 +630,10 @@ body { .think-item-row { border-left: none; - padding-left: 0; + padding: 8px 10px; + border: 1px solid var(--chat-border); + border-radius: 10px; + background: rgba(17, 17, 17, 0.02); display: flex; flex-direction: column; gap: 6px; @@ -668,6 +678,47 @@ body { line-height: 1.6; } +html[data-theme='dark'] .think-agent { + background: #101924; + border-color: #273142; +} + +html[data-theme='dark'] .think-agent summary { + background: transparent; +} + +html[data-theme='dark'] .think-item-row { + background: #0f1824; + border-color: #273142; +} + +html[data-theme='dark'] .think-item-type { + color: #9fb2c9; +} + +html[data-theme='dark'] .think-item-body { + color: #d5e0ee; +} + +@keyframes thinkGlowDark { + 0% { + box-shadow: 0 0 0 rgba(96, 165, 250, 0); + background: transparent; + } + 50% { + box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2); + background: rgba(96, 165, 250, 0.08); + } + 100% { + box-shadow: 0 0 0 rgba(96, 165, 250, 0); + background: transparent; + } +} + +html[data-theme='dark'] .think-block[data-thinking="true"] .think-agent summary { + animation: thinkGlowDark 1.6s ease-in-out infinite; +} + .composer-shell { position: sticky; bottom: 8px; diff --git a/app/static/public/js/chat.js b/app/static/public/js/chat.js index 3fe7596..6344b4f 100644 --- a/app/static/public/js/chat.js +++ b/app/static/public/js/chat.js @@ -799,49 +799,37 @@ const ids = Array.from(new Set(list.map((b) => String(b.id || '').trim()).filter(Boolean))); const nonGeneralIds = ids.filter((id) => !/^general$/i.test(id)); - // 优先按 rollout id 分组,能更稳定地映射多 agent。 - if (nonGeneralIds.length >= 2) { - const groups = []; - const map = new Map(); - for (const block of list) { - const key = String(block.id || 'General'); - let group = map.get(key); - if (!group) { - group = { key, blocks: [] }; - map.set(key, group); - groups.push(group); - } - group.blocks.push(block); - } - return groups.map((group, idx) => ({ - title: idx === 0 ? 'Grok Leader' : `Agent ${idx}`, - blocks: group.blocks - })); + if (nonGeneralIds.length <= 1) { + return []; } - - // 兜底:按 AgentThink 作为阶段收束点切分。 - const sections = []; - let current = []; + const groups = []; + const map = new Map(); for (const block of list) { - current.push(block); - if (/^agentthink$/i.test(String(block.type || '').trim())) { - sections.push(current); - current = []; + const key = String(block.id || 'General'); + let group = map.get(key); + if (!group) { + group = { key, blocks: [] }; + map.set(key, group); + groups.push(group); } + group.blocks.push(block); } - if (current.length) { - sections.push(current); - } - - if (sections.length <= 1) { - return [{ title: 'Grok Leader', blocks: list }]; - } - return sections.map((part, idx) => ({ + return groups.map((group, idx) => ({ title: idx === 0 ? 'Grok Leader' : `Agent ${idx}`, - blocks: part + blocks: group.blocks })); } + function renderFlatBlocks(blocks) { + return (Array.isArray(blocks) ? blocks : []).map((item) => { + const body = renderBasicMarkdown((item.lines || []).join('\n').trim()); + const typeText = escapeHtml(item.type); + const typeKey = String(item.type || '').trim().toLowerCase().replace(/\s+/g, ''); + const typeAttr = escapeHtml(typeKey); + return `
${typeText}
${body || '(空)'}
`; + }).join(''); + } + function renderThinkContent(text, openAll) { const sections = parseAgentSections(text); if (!sections.length) { @@ -878,9 +866,9 @@ const blocks = parseRolloutBlocks(section.lines.join('\n'), section.title || 'General'); if (!section.title && blocks.length) { const synthetic = splitBlocksIntoSyntheticAgents(blocks); - if (synthetic.length > 1) { + if (synthetic.length) { return synthetic.map((agent, agentIdx) => { - const inner = renderGroups(agent.blocks, openAll); + const inner = renderFlatBlocks(agent.blocks); const title = escapeHtml(agent.title); const openAttr = openAll ? ' open' : (idx === 0 && agentIdx === 0 ? ' open' : ''); return `
${title}
${inner}
`; @@ -1126,6 +1114,27 @@ if (!entry) return; entry.raw = content || ''; if (!entry.contentNode) return; + const captureOpenState = (root, selector) => { + if (!root || !root.querySelectorAll) return null; + const nodes = Array.from(root.querySelectorAll(selector)); + if (!nodes.length) return null; + return nodes.map((node) => node.hasAttribute('open')); + }; + const restoreOpenState = (root, selector, states) => { + if (!root || !root.querySelectorAll || !Array.isArray(states) || !states.length) return; + const nodes = Array.from(root.querySelectorAll(selector)); + const max = Math.min(nodes.length, states.length); + for (let i = 0; i < max; i += 1) { + if (states[i]) { + nodes[i].setAttribute('open', ''); + } else { + nodes[i].removeAttribute('open'); + } + } + }; + const savedThinkBlockState = captureOpenState(entry.contentNode, '.think-block'); + const savedThinkAgentState = captureOpenState(entry.contentNode, '.think-agent'); + const savedRolloutState = captureOpenState(entry.contentNode, '.think-rollout-group'); if (!entry.hasThink && entry.raw.includes('')) { entry.hasThink = true; } @@ -1139,7 +1148,13 @@ entry.contentNode.textContent = entry.raw; } } + restoreOpenState(entry.contentNode, '.think-block', savedThinkBlockState); + restoreOpenState(entry.contentNode, '.think-agent', savedThinkAgentState); + restoreOpenState(entry.contentNode, '.think-rollout-group', savedRolloutState); if (entry.hasThink) { + if (finalize && (entry.thinkElapsed === null || typeof entry.thinkElapsed === 'undefined')) { + entry.thinkElapsed = 0; + } updateThinkSummary(entry, entry.thinkElapsed); } if (entry.role === 'assistant' || entry.role === 'user') {