fix: 优化思考流稳定性与 agent 区块交互样式
This commit is contained in:
1 parent
5c0bb6fb4e
commit
84f365dba3
3 files changed
+139
-53
No files matched your search
@@ -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("<think>\n")
|
||||
self.think_opened = True
|
||||
if (not is_thinking) and self.think_opened:
|
||||
yield self._sse("\n</think>\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:
|
||||
# 避免上游字面 <think> 标签与前端思考分段逻辑冲突,导致多段思考块
|
||||
filtered = re.sub(r"</?think>", "", 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("<think>\n")
|
||||
self.think_opened = True
|
||||
else:
|
||||
if self.think_opened:
|
||||
yield self._sse("\n</think>\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("</think>\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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 `<div class="think-item-row"><div class="think-item-type" data-type="${typeAttr}">${typeText}</div><div class="think-item-body">${body || '<em>(空)</em>'}</div></div>`;
|
||||
}).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 `<details class="think-agent"${openAttr}><summary>${title}</summary><div class="think-agent-items">${inner}</div></details>`;
|
||||
@@ -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('<think>')) {
|
||||
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') {
|
||||
|
||||
Reference in new issue
Block a user