feat: 支持 MCP 图片内联渲染

This commit is contained in:
shiyue
2026-08-06 13:37:42 +08:00
parent 3f00cfe7a4
commit 3ddf21af85
6 changed files with 237 additions and 3 deletions

View File

@@ -7867,6 +7867,9 @@
hydrateRenderedMarkdown(textDiv);
} else if (block.type === 'todo_list') {
bubble.appendChild(createTodoListElement(block));
} else if (block.type === 'image') {
const image = createAssistantImageElement(block);
if (image) bubble.appendChild(image);
}
});
return;
@@ -7875,6 +7878,41 @@
setRenderedMarkdown(bubble, String(content));
}
function createAssistantImageElement(block = {}) {
const mimeType = String(block.mimeType || block.media_type || 'image/png').trim();
const data = String(block.data || '').replace(/\s+/g, '');
const url = String(block.url || block.source || block.image_url?.url || block.imageUrl || '').trim()
|| (data ? `data:${mimeType};base64,${data}` : '');
if (!url || (!/^https?:\/\//i.test(url) && !/^data:image\//i.test(url))) return null;
const alt = String(block.alt || block.title || '图片').trim() || '图片';
const figure = document.createElement('figure');
figure.className = 'assistant-image';
const button = document.createElement('button');
button.type = 'button';
button.className = 'assistant-image-button';
button.setAttribute('aria-label', `放大查看:${alt}`);
const img = document.createElement('img');
img.src = url;
img.alt = alt;
img.loading = 'lazy';
img.decoding = 'async';
button.appendChild(img);
button.addEventListener('click', () => openAttachmentPreviewModal({
id: `assistant-image-${createLocalId('preview')}`,
filename: String(block.title || alt),
previewUrl: url,
size: data ? Math.floor(data.length * 0.75) : 0,
}));
figure.appendChild(button);
if (block.title) {
const caption = document.createElement('figcaption');
caption.textContent = String(block.title);
figure.appendChild(caption);
}
return figure;
}
function createTodoListElement(block) {
const container = document.createElement('div');
container.className = 'todo-list-container';
@@ -9094,7 +9132,8 @@
function isGroupableToolCall(node) {
return !!(node?.classList?.contains('tool-call')
&& node.dataset.toolKind !== 'todo_list'
&& node.dataset.toolKind !== 'collab_agent_tool_call');
&& node.dataset.toolKind !== 'collab_agent_tool_call'
&& !node.classList.contains('ccweb-display-image-tool'));
}
function rememberToolCallTarget(toolUseId, tool, element) {
@@ -9429,6 +9468,10 @@
const effectiveResult = tool.result;
const kind = toolKind(tool);
if (isCcwebDisplayImageTool(tool)) {
return createCcwebDisplayImageToolContent(tool);
}
if (kind === 'todo_list') {
let todoData = effectiveInput;
// 如果有 result 且是字符串,尝试解析
@@ -9506,8 +9549,48 @@
return content;
}
function isCcwebDisplayImageTool(tool = {}) {
const inputTool = String(tool?.input?.tool || tool?.input?.name || '');
const subtitle = String(tool?.meta?.subtitle || '');
return inputTool === 'ccweb_display_image' || /(?:^|\.)ccweb_display_image$/.test(subtitle);
}
function parseCcwebDisplayImageResult(result) {
if (result && typeof result === 'object') return result;
try { return JSON.parse(String(result || '')); } catch { return null; }
}
function createCcwebDisplayImageToolContent(tool = {}) {
const wrapper = document.createElement('div');
wrapper.className = 'tool-call-content ccweb-display-image-content';
const payload = parseCcwebDisplayImageResult(tool.result);
if (!payload?.ok) {
wrapper.textContent = payload?.message || (tool.done ? '图片渲染失败' : '正在准备图片…');
return wrapper;
}
if (payload.attachment?.id) {
wrapper.insertAdjacentHTML('beforeend', renderAttachmentPreviews([payload.attachment]));
requestAnimationFrame(() => hydrateAttachmentPreviews(wrapper, [payload.attachment]));
return wrapper;
}
const image = createAssistantImageElement(payload.image || {});
if (image) wrapper.appendChild(image);
else wrapper.textContent = '图片来源不可用';
return wrapper;
}
function createToolCallElement(toolUseId, tool, done) {
const kind = toolKind(tool);
if (isCcwebDisplayImageTool(tool)) {
const wrapper = document.createElement('div');
wrapper.className = 'tool-call ccweb-display-image-tool';
wrapper.id = `tool-node-${++toolDomSeq}`;
wrapper.dataset.toolUseId = toolUseId ? String(toolUseId) : '';
wrapper.dataset.toolName = tool.name || '';
wrapper.dataset.toolKind = kind || 'mcp_tool_call';
wrapper.appendChild(buildToolContentElement({ ...tool, done }));
return wrapper;
}
if (kind === 'collab_agent_tool_call') {
const wrapper = document.createElement('div');
wrapper.className = 'tool-call ccweb-mcp-child-agent-tool-call collab-agent-inline';