fix: 修复聊天图片在思考块后的显示位置

This commit is contained in:
XianYuDaXian committed 2026-03-24 14:29:46 +08:00
1 parent e49ce32fc2
commit 79af9c63eb
3 files changed
+65 -29

No files matched your search

+22 -29
View File
@@ -551,9 +551,8 @@ class StreamProcessor(proc_base.BaseProcessor):
self.rollout_id: str = ""
self.fingerprint: str = ""
self.think_opened: bool = False
self.seen_think_once: bool = False
self.think_closed_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 = (
@@ -758,6 +757,13 @@ class StreamProcessor(proc_base.BaseProcessor):
}
return f"data: {orjson.dumps(chunk).decode()}\n\n"
async def _close_think_block(self) -> AsyncGenerator[str, None]:
"""Close the visible think block before emitting normal answer content."""
if self.think_opened:
yield self._sse("\n</think>\n")
self.think_opened = False
self.think_closed_once = True
async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, None]:
"""Process stream response.
@@ -805,7 +811,6 @@ class StreamProcessor(proc_base.BaseProcessor):
if not self.think_opened:
yield self._sse("<think>\n")
self.think_opened = True
self.seen_think_once = True
idx = img.get("imageIndex", 0) + 1
progress = img.get("progress", 0)
yield self._sse(
@@ -814,6 +819,9 @@ class StreamProcessor(proc_base.BaseProcessor):
continue
if mr := resp.get("modelResponse"):
if self.image_think_active and self.think_opened:
async for chunk in self._close_think_block():
yield chunk
self.image_think_active = False
for url in proc_base._collect_images(mr):
parts = url.split("/")
@@ -844,6 +852,8 @@ class StreamProcessor(proc_base.BaseProcessor):
original = image.get("original")
title = image.get("title") or ""
if original:
async for chunk in self._close_think_block():
yield chunk
title_safe = title.replace("\n", " ").strip()
if title_safe:
yield self._sse(f"![{title_safe}]({original})\n")
@@ -854,6 +864,8 @@ class StreamProcessor(proc_base.BaseProcessor):
if (token := resp.get("token")) is not None:
if not token:
continue
if is_thinking and self.think_closed_once and not self.image_think_active:
continue
filtered = self._filter_token(token)
if not filtered:
continue
@@ -862,7 +874,7 @@ class StreamProcessor(proc_base.BaseProcessor):
if not filtered:
continue
in_think = (
is_thinking
(is_thinking and not self.think_closed_once)
or self.image_think_active
)
if in_think:
@@ -871,14 +883,11 @@ class StreamProcessor(proc_base.BaseProcessor):
if not self.think_opened:
yield self._sse("<think>\n")
self.think_opened = True
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
if self.think_opened:
async for chunk in self._close_think_block():
yield chunk
if self._tool_stream_enabled:
for kind, payload in self._handle_tool_stream(filtered):
@@ -891,25 +900,9 @@ class StreamProcessor(proc_base.BaseProcessor):
yield self._sse(filtered)
if self.think_opened:
yield self._sse("</think>\n")
self.think_opened = False
if self.answer_buffer:
buffered = "".join(self.answer_buffer)
if self._tool_stream_enabled:
for kind, payload in self._handle_tool_stream(buffered):
if kind == "text":
yield self._sse(payload)
elif kind == "tool":
yield self._sse(tool_calls=[payload])
for kind, payload in self._flush_tool_stream():
if kind == "text":
yield self._sse(payload)
elif kind == "tool":
yield self._sse(tool_calls=[payload])
else:
yield self._sse(buffered)
self.answer_buffer = []
if self._tool_stream_enabled and not self.answer_buffer:
async for chunk in self._close_think_block():
yield chunk
if self._tool_stream_enabled:
for kind, payload in self._flush_tool_stream():
if kind == "text":
yield self._sse(payload)
+4
View File
@@ -429,6 +429,10 @@ body {
padding-right: 0;
}
.think-image-extract {
margin: 12px 0 16px;
}
.img-grid {
display: grid;
gap: 2px;
+39
View File
@@ -1109,6 +1109,44 @@
});
}
function liftThinkImages(root) {
if (!root || !root.querySelectorAll) return;
const thinkBlocks = Array.from(root.querySelectorAll('.think-block'));
thinkBlocks.forEach((block, blockIndex) => {
const images = Array.from(block.querySelectorAll('.think-content img'));
if (!images.length) return;
let gallery = block.nextElementSibling;
if (!(gallery instanceof HTMLElement) || !gallery.classList.contains('think-image-extract')) {
gallery = document.createElement('div');
gallery.className = 'think-image-extract';
gallery.dataset.thinkBlockIndex = String(blockIndex);
block.insertAdjacentElement('afterend', gallery);
}
images.forEach((img) => {
const paragraph = img.closest('p');
gallery.appendChild(img);
if (paragraph) {
const residue = (paragraph.textContent || '').replace(/\s+/g, '');
if (!residue || /^\.(?:png|jpe?g|webp|gif)\)?$/i.test(residue)) {
paragraph.remove();
return;
}
}
const nextText = img.nextSibling;
if (nextText && nextText.nodeType === Node.TEXT_NODE) {
nextText.textContent = String(nextText.textContent || '').replace(/^\s*\.(?:png|jpe?g|webp|gif)\)?/i, '');
if (!nextText.textContent.trim()) {
nextText.parentNode && nextText.parentNode.removeChild(nextText);
}
}
});
});
}
function bindCodeCopyButtons(root) {
if (!root || !root.querySelectorAll) return;
const buttons = root.querySelectorAll('.code-copy-btn');
@@ -1216,6 +1254,7 @@
updateThinkSummary(entry, entry.thinkElapsed);
}
if (entry.role === 'assistant' || entry.role === 'user') {
liftThinkImages(entry.contentNode);
applyImageGrid(entry.contentNode);
enhanceBrokenImages(entry.contentNode);
bindMessageImagePreview(entry.contentNode);