feat: 支持 MCP 图片内联渲染
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user