agent-Specialization/static/src/app/bootstrap.ts

1292 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import katex from 'katex';
import DOMPurify from 'dompurify';
let showImageRenderSeq = 0;
let showImageDebugLogCount = 0;
const SHOW_IMAGE_DEBUG_VERSION = 'v2-lite-throttled';
const SHOW_IMAGE_DEBUG_MAX_LOGS = 200;
let showHtmlDebugLogCount = 0;
const SHOW_HTML_DEBUG_MAX_LOGS = 500;
function getShowImageDebugMode(): 'off' | 'lite' | 'verbose' {
if (typeof window === 'undefined') return 'off';
try {
const explicitFlag = (window as any).__SHOW_IMAGE_DEBUG__;
if (
explicitFlag === false ||
explicitFlag === '0' ||
explicitFlag === 'off' ||
explicitFlag === 'false'
) {
return 'off';
}
if (explicitFlag === 'verbose') return 'verbose';
if (explicitFlag === true || explicitFlag === '1' || explicitFlag === 'lite') return 'lite';
const localFlag = window.localStorage?.getItem('showImageDebug');
if (localFlag === '0' || localFlag === 'off' || localFlag === 'false' || localFlag === 'none') {
return 'off';
}
if (localFlag === 'verbose') return 'verbose';
if (localFlag === '1' || localFlag === 'true' || localFlag === 'lite') return 'lite';
return 'off';
} catch {
return 'off';
}
}
function debugShowImageLog(event: string, payload: Record<string, any> = {}) {
const mode = getShowImageDebugMode();
if (mode === 'off') return;
if (
mode === 'lite' &&
!['observer:setup', 'observer:show-image-mutation', 'render:start', 'render:end'].includes(
event
)
) {
return;
}
if (showImageDebugLogCount >= SHOW_IMAGE_DEBUG_MAX_LOGS) return;
showImageDebugLogCount += 1;
if (showImageDebugLogCount === SHOW_IMAGE_DEBUG_MAX_LOGS) {
console.warn('[SHOW_IMAGE_DEBUG]', 'log-limit-reached', {
max: SHOW_IMAGE_DEBUG_MAX_LOGS,
version: SHOW_IMAGE_DEBUG_VERSION
});
return;
}
console.log('[SHOW_IMAGE_DEBUG]', event, payload);
}
function isShowHtmlDebugEnabled() {
if (typeof window === 'undefined') return true;
try {
const explicitFlag = (window as any).__SHOW_HTML_DEBUG__;
if (explicitFlag === false || explicitFlag === '0') return false;
if (explicitFlag === true || explicitFlag === '1') return true;
const localFlag = window.localStorage?.getItem('showHtmlDebug');
if (localFlag === '0' || localFlag === 'false') return false;
if (localFlag === '1' || localFlag === 'true') return true;
return true;
} catch {
return true;
}
}
function debugShowHtmlLog(event: string, payload: Record<string, any> = {}) {
if (!isShowHtmlDebugEnabled()) return;
if (showHtmlDebugLogCount >= SHOW_HTML_DEBUG_MAX_LOGS) return;
showHtmlDebugLogCount += 1;
if (showHtmlDebugLogCount === SHOW_HTML_DEBUG_MAX_LOGS) {
console.warn('[SHOW_HTML_DEBUG]', 'log-limit-reached', {
max: SHOW_HTML_DEBUG_MAX_LOGS
});
return;
}
console.log('[SHOW_HTML_DEBUG]', event, payload);
}
function markShowTagDrawingActive(holdMs = 1600) {
if (typeof window === 'undefined') return;
try {
(window as any).__SHOW_TAG_DRAWING_UNTIL__ = Date.now() + holdMs;
} catch {
// ignore
}
}
function summarizeNodeForDebug(node: Node | null) {
if (!node) return 'null';
if (node.nodeType === Node.TEXT_NODE) {
const text = (node.textContent || '').replace(/\s+/g, ' ').trim();
return `TEXT(${text.slice(0, 80)})`;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
const tag = el.tagName.toLowerCase();
if (tag === 'show_image' || tag === 'show-image') {
return `SHOW_IMAGE(src=${el.getAttribute('src') || ''})`;
}
return `${tag.toUpperCase()}`;
}
return `NODE(${node.nodeType})`;
}
function collectShowImageOrder(root: ParentNode | null = document) {
if (!root || !(root instanceof Node)) return [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ALL);
const tokens: string[] = [];
let current = walker.currentNode;
while (current) {
if (current.nodeType === Node.TEXT_NODE) {
const text = (current.textContent || '').replace(/\s+/g, ' ').trim();
if (text) tokens.push(`TEXT(${text.slice(0, 40)})`);
} else if (current.nodeType === Node.ELEMENT_NODE) {
const el = current as Element;
const tag = el.tagName.toLowerCase();
if (tag === 'show_image' || tag === 'show-image') {
tokens.push(`SHOW_IMAGE(${el.getAttribute('src') || ''})`);
} else if (tag === 'show_html' || tag === 'show-html') {
tokens.push(`SHOW_HTML(ratio=${readShowHtmlRatio(el)},js=${readShowHtmlJsMode(el)})`);
}
}
current = walker.nextNode();
}
return tokens.slice(0, 200);
}
function normalizeShowImageSrc(src: string) {
if (!src) return '';
const trimmed = src.trim();
if (/^https?:\/\//i.test(trimmed)) return trimmed;
if (trimmed.startsWith('/user_upload/')) return trimmed;
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
if (idx >= 0) {
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
}
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
return trimmed;
}
return '';
}
function parsePixelSize(raw: string | null, fallback: number) {
if (!raw) return fallback;
const n = Number.parseInt(raw.trim(), 10);
if (!Number.isFinite(n) || n <= 0) return fallback;
return Math.min(4096, n);
}
const SHOW_HTML_ALLOWED_RATIOS = ['1:1', '16:9', '9:16', '4:3', '3:4'] as const;
const SHOW_HTML_RATIO_VALUES: Record<string, number> = {
'1:1': 1,
'16:9': 16 / 9,
'9:16': 9 / 16,
'4:3': 4 / 3,
'3:4': 3 / 4
};
// show_html 统一按比例对应预设基准像素渲染(前端仍会按可用空间自适应)
const SHOW_HTML_RATIO_BASE_SIZE: Record<string, { width: number; height: number }> = {
'1:1': { width: 620, height: 620 },
'16:9': { width: 620, height: 349 },
'9:16': { width: 620, height: 1102 },
'4:3': { width: 620, height: 465 },
'3:4': { width: 620, height: 827 }
};
function normalizeShowHtmlRatio(raw: string | null) {
if (!raw) return '1:1';
const cleaned = raw.trim().toLowerCase().replace(/[x/]/g, ':').replace(/\s+/g, '');
if ((SHOW_HTML_ALLOWED_RATIOS as readonly string[]).includes(cleaned)) return cleaned;
return '1:1';
}
function pickNearestAllowedRatio(width: number, height: number) {
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return '1:1';
}
const target = width / height;
let best = '1:1';
let bestDiff = Number.POSITIVE_INFINITY;
for (const ratio of SHOW_HTML_ALLOWED_RATIOS) {
const diff = Math.abs(SHOW_HTML_RATIO_VALUES[ratio] - target);
if (diff < bestDiff) {
bestDiff = diff;
best = ratio;
}
}
return best;
}
function readShowHtmlRatio(node: Element) {
const ratioAttr = node.getAttribute('ratio');
if (ratioAttr) return normalizeShowHtmlRatio(ratioAttr);
// 兼容旧格式:模型输出了 width/height 时,自动映射到允许的比例
const width = parsePixelSize(node.getAttribute('width'), 0);
const height = parsePixelSize(node.getAttribute('height'), 0);
if (width > 0 && height > 0) return pickNearestAllowedRatio(width, height);
return '1:1';
}
function normalizeShowHtmlJsMode(raw: string | null) {
if (!raw) return 'off';
const cleaned = raw.trim().toLowerCase();
if (['on', '1', 'true', 'yes', 'enabled', 'enable'].includes(cleaned)) {
return 'on';
}
return 'off';
}
function readShowHtmlJsMode(node: Element) {
return normalizeShowHtmlJsMode(node.getAttribute('js'));
}
function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) {
const ratio = SHOW_HTML_RATIO_VALUES[ratioKey] || 1;
const base = SHOW_HTML_RATIO_BASE_SIZE[ratioKey] || SHOW_HTML_RATIO_BASE_SIZE['1:1'];
const windowW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 320);
const windowH = Math.max(360, window.innerHeight || document.documentElement.clientHeight || 360);
const parentRect = node.parentElement?.getBoundingClientRect();
const flowRect =
node.closest('.messages-flow')?.getBoundingClientRect() ||
node.closest('.message-text')?.getBoundingClientRect() ||
node.closest('.text-content')?.getBoundingClientRect() ||
node.closest('.messages-area')?.getBoundingClientRect() ||
null;
const availableWidth = Math.max(
220,
Math.floor(Math.min(flowRect?.width || windowW, parentRect?.width || windowW) - 8)
);
// 移除“二次约束”后:窗口/容器足够大时,严格使用 prompt 约定的基准像素;
// 仅在物理空间不足时按比例等比缩小(不再做额外的最小/最大高度钳制)。
// 最大卡片约束:上限按 1200x900窗口不足时仍按可用空间缩放
const limitWidth = Math.max(220, Math.min(availableWidth, Math.floor(windowW * 0.96), 1200));
const limitHeight = Math.max(180, Math.min(Math.floor(windowH * 0.9), 900));
const widthScale = limitWidth / base.width;
const heightScale = limitHeight / base.height;
const scale = Math.min(1, widthScale, heightScale);
const widthPx = Math.max(220, Math.round(base.width * scale));
const heightPx = Math.round(widthPx / ratio);
return { widthPx, heightPx, ratio };
}
function decodeBase64Utf8(input: string) {
if (!input) return '';
try {
return decodeURIComponent(escape(atob(input)));
} catch {
return '';
}
}
function sanitizeInlineCss(css: string) {
if (!css) return '';
return css
.replace(/expression\s*\([^)]*\)/gi, '')
.replace(/url\s*\(\s*['"]?\s*javascript:[^)]+\)/gi, 'url(about:blank)')
.replace(/@import[^;]+;/gi, '');
}
function rewriteCssSelectorsForShadowDom(css: string) {
if (!css) return '';
// 注意:不能直接把 html/body 连续替换成 .show-html-root
// 否则后续 /\bhtml\b/ 会再次命中 ".show-html-root" 里的 "html",产生
// 类似 ".show-.show-html-root-root" 的错误选择器,导致布局样式失效(元素偏移)。
const placeholder = '__SHOW_HTML_ROOT_SELECTOR__';
return css
.replace(/\bhtml\s*,\s*body\b/gi, placeholder)
.replace(/\bbody\s*,\s*html\b/gi, placeholder)
.replace(/\bhtml\b/gi, placeholder)
.replace(/\bbody\b/gi, placeholder)
.replace(new RegExp(placeholder, 'g'), '.show-html-root');
}
function sanitizeShowHtmlContent(rawHtml: string) {
const purified = DOMPurify.sanitize(rawHtml || '', {
USE_PROFILES: { html: true, svg: true, svgFilters: true, mathMl: true },
FORBID_TAGS: ['script', 'noscript', 'iframe', 'object', 'embed', 'link'],
ALLOW_DATA_ATTR: false,
ALLOW_ARIA_ATTR: true
}) as string;
const parser = new DOMParser();
const doc = parser.parseFromString(`<body>${purified}</body>`, 'text/html');
const elements = Array.from(doc.body.querySelectorAll('*'));
elements.forEach((el) => {
Array.from(el.attributes).forEach((attr) => {
const name = attr.name.toLowerCase();
const value = attr.value || '';
if (name.startsWith('on')) {
el.removeAttribute(attr.name);
return;
}
if (
(name === 'src' || name === 'href' || name === 'xlink:href') &&
/^\s*javascript:/i.test(value)
) {
el.removeAttribute(attr.name);
return;
}
if (name === 'style') {
el.setAttribute('style', sanitizeInlineCss(value));
}
});
});
doc.querySelectorAll('style').forEach((styleEl) => {
styleEl.textContent = rewriteCssSelectorsForShadowDom(
sanitizeInlineCss(styleEl.textContent || '')
);
});
return doc.body.innerHTML;
}
function buildShowHtmlIframeSrcdoc(rawHtml: string) {
const content = (rawHtml || '').trim();
if (!content) {
return '<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
}
// 若模型已经输出完整 HTML 文档,直接使用;否则包裹成最小可运行文档。
if (/<html[\s>]/i.test(content)) {
return content;
}
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
min-height: 100%;
background: transparent;
scrollbar-width: thin;
scrollbar-color: rgba(121, 109, 94, 0.48) transparent;
}
html::-webkit-scrollbar, body::-webkit-scrollbar {
width: 8px;
height: 8px;
}
html::-webkit-scrollbar-track, body::-webkit-scrollbar-track {
background: transparent;
}
html::-webkit-scrollbar-thumb, body::-webkit-scrollbar-thumb {
background: rgba(121, 109, 94, 0.48);
border-radius: 8px;
}
html::-webkit-scrollbar-thumb:hover, body::-webkit-scrollbar-thumb:hover {
background: rgba(121, 109, 94, 0.62);
}
</style>
</head>
<body>${content}</body>
</html>`;
}
function escapeHtml(input: string) {
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderShowImages(root: ParentNode | null = document) {
if (!root) return;
const debugMode = getShowImageDebugMode();
const verbose = debugMode === 'verbose';
const renderId = ++showImageRenderSeq;
const imageNodes = Array.from(
root.querySelectorAll('show_image:not([data-rendered]), show-image:not([data-rendered])')
);
const htmlNodes = Array.from(
root.querySelectorAll('show_html:not([data-rendered]), show-html:not([data-rendered])')
);
if (htmlNodes.length > 0) {
markShowTagDrawingActive();
}
if (!verbose && imageNodes.length === 0 && htmlNodes.length === 0) return;
const rootNode = root as Node;
debugShowImageLog('render:start', {
renderId,
rootType:
rootNode?.nodeType === Node.ELEMENT_NODE
? (rootNode as Element).tagName.toLowerCase()
: `nodeType:${rootNode?.nodeType}`,
pendingImageCount: imageNodes.length,
pendingHtmlCount: htmlNodes.length,
orderBefore: collectShowImageOrder(root)
});
if (verbose) {
debugShowImageLog('render:pending-show-images', {
renderId,
count: imageNodes.length,
nodes: imageNodes.map((n) => n.getAttribute('src') || '')
});
}
imageNodes.forEach((node) => {
if (!node.isConnected || node.hasAttribute('data-rendered')) return;
if (verbose) {
debugShowImageLog('render:node-start', {
renderId,
src: node.getAttribute('src') || '',
parent: summarizeNodeForDebug(node.parentNode),
prevSibling: summarizeNodeForDebug(node.previousSibling),
nextSibling: summarizeNodeForDebug(node.nextSibling),
childCount: node.childNodes.length,
childPreview: Array.from(node.childNodes)
.slice(0, 6)
.map((child) => summarizeNodeForDebug(child))
});
}
// 将 show_image 内误被包裹的内容移动到当前节点之后,保持原有顺序
if (node.parentNode && node.firstChild) {
const parent = node.parentNode;
const ref = node.nextSibling; // 可能为 nullinsertBefore 会当 append
const children = Array.from(node.childNodes);
if (verbose) {
debugShowImageLog('render:unwrap-children', {
renderId,
src: node.getAttribute('src') || '',
moveCount: children.length,
ref: summarizeNodeForDebug(ref),
children: children.slice(0, 10).map((child) => summarizeNodeForDebug(child))
});
}
children.forEach((child) => parent.insertBefore(child, ref));
}
const rawSrc = node.getAttribute('src') || '';
const mappedSrc = normalizeShowImageSrc(rawSrc);
if (!mappedSrc) {
node.setAttribute('data-rendered', '1');
node.setAttribute('data-rendered-error', 'invalid-src');
if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc });
return;
}
const alt = node.getAttribute('alt') || '';
const safeAlt = escapeHtml(alt.trim());
const figure = document.createElement('figure');
figure.className = 'chat-inline-card chat-inline-card--image chat-inline-image';
const img = document.createElement('img');
img.loading = 'lazy';
img.src = mappedSrc;
img.alt = safeAlt;
img.onerror = () => {
figure.classList.add('chat-inline-card--error', 'chat-inline-image--error');
const tip = document.createElement('div');
tip.className = 'chat-inline-card__error chat-inline-image__error';
tip.textContent = '图片加载失败';
figure.appendChild(tip);
};
figure.appendChild(img);
if (safeAlt) {
const caption = document.createElement('figcaption');
caption.className = 'chat-inline-card__caption';
caption.innerHTML = safeAlt;
figure.appendChild(caption);
}
node.replaceChildren(figure);
node.setAttribute('data-rendered', '1');
if (verbose) {
debugShowImageLog('render:node-done', {
renderId,
rawSrc,
mappedSrc,
alt
});
}
});
htmlNodes.forEach((node) => {
markShowTagDrawingActive();
if (!node.isConnected) return;
const inStreaming = !!node.closest('.streaming-text');
const pathKey = buildNodePathKey(node);
const isPartial = node.getAttribute('data-partial') === '1';
const encoded = node.getAttribute('data-encoded') || '';
const jsMode = readShowHtmlJsMode(node);
const computedRatioKey = readShowHtmlRatio(node);
let ratioKey = computedRatioKey;
let { widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey);
// 流式阶段:一旦 show_html 已经出现完整闭合(非 partial冻结该 path 的已完成快照;
// 后续即使继续输出其他普通文本,也复用该快照,避免已完成 show_html 反复刷新。
if (inStreaming && !isPartial && encoded) {
const frozen = showHtmlCompletedSnapshotByPath.get(pathKey);
if (!frozen || frozen.encoded !== encoded || frozen.ratioKey !== ratioKey) {
showHtmlCompletedSnapshotByPath.set(pathKey, {
encoded,
ratioKey
});
}
} else if (!inStreaming) {
showHtmlCompletedSnapshotByPath.delete(pathKey);
}
const frozenSnapshot = inStreaming ? showHtmlCompletedSnapshotByPath.get(pathKey) : null;
const effectiveEncoded = frozenSnapshot?.encoded || encoded;
if (frozenSnapshot?.ratioKey) {
ratioKey = frozenSnapshot.ratioKey;
({ widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey));
}
if (inStreaming) {
const locked = showHtmlStreamingSizeLockByPath.get(pathKey);
if (locked) {
// 修复:流式初期若 ratio 解析不完整会先落到 1:1后续必须允许升级到真实比例
const shouldUpgradeRatio =
ratioKey !== locked.ratioKey && (locked.ratioKey === '1:1' || ratioKey !== '1:1');
if (shouldUpgradeRatio) {
ratioKey = computedRatioKey;
({ widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey));
showHtmlStreamingSizeLockByPath.set(pathKey, {
ratioKey,
widthPx,
heightPx
});
} else {
ratioKey = locked.ratioKey;
widthPx = locked.widthPx;
heightPx = locked.heightPx;
}
} else {
showHtmlStreamingSizeLockByPath.set(pathKey, {
ratioKey,
widthPx,
heightPx
});
}
} else {
showHtmlStreamingSizeLockByPath.delete(pathKey);
}
// streaming 阶段 v-html 会反复重建 show_html 节点,这里先锁定占位尺寸,避免高度掉 0
node.style.display = 'block';
node.style.width = `${widthPx}px`;
node.style.maxWidth = '100%';
node.style.minHeight = `${heightPx}px`;
node.style.margin = '14px auto';
node.style.boxSizing = 'border-box';
node.style.overflow = 'hidden';
debugShowHtmlLog('render:node-seen', {
renderId,
inStreaming,
pathKey,
hasRendered: node.hasAttribute('data-rendered'),
ratioAttr: node.getAttribute('ratio') || '',
jsMode,
ratioKey,
partial: node.getAttribute('data-partial') || '',
encodedLength: encoded.length,
effectiveEncodedLength: effectiveEncoded.length,
innerLength: (node.innerHTML || '').length,
reservedWidth: `${widthPx}px`,
reservedMinHeight: `${heightPx}px`
});
// js="on":在闭合前只显示占位,不做实时渲染,避免 iframe/srcdoc 反复重建与闪烁
if (jsMode === 'on' && isPartial) {
let pending = showHtmlJsPendingRenderByPath.get(pathKey);
if (!pending) {
const wrapper = document.createElement('div');
wrapper.className =
'chat-inline-card chat-inline-card--html chat-inline-card--pending chat-inline-html chat-inline-html--pending';
const host = document.createElement('div');
host.className = 'chat-inline-card__body chat-inline-html__host chat-inline-html__host--pending';
const tip = document.createElement('div');
tip.className = 'chat-inline-card__pending-tip chat-inline-html__pending-tip';
tip.textContent = '正在渲染内容...';
host.appendChild(tip);
wrapper.appendChild(host);
pending = {
wrapper,
host,
tip,
widthPx: 0,
heightPx: 0,
ratioKey: '1:1'
};
showHtmlJsPendingRenderByPath.set(pathKey, pending);
}
pending.wrapper.style.width = `${widthPx}px`;
pending.host.style.height = `${heightPx}px`;
pending.widthPx = widthPx;
pending.heightPx = heightPx;
pending.ratioKey = ratioKey;
node.replaceChildren(pending.wrapper);
node.setAttribute('data-rendered', '1');
debugShowHtmlLog('render:node-js-pending', {
renderId,
pathKey,
ratioKey,
widthPx,
heightPx
});
return;
}
// js="on":闭合后使用 sandbox iframe 隔离执行(不依赖 shadow root 直渲染)。
if (jsMode === 'on') {
const rawHtml = effectiveEncoded ? decodeBase64Utf8(effectiveEncoded) : node.innerHTML || '';
const srcdoc = buildShowHtmlIframeSrcdoc(rawHtml);
let persistent = showHtmlJsIframeRenderByPath.get(pathKey);
if (!persistent) {
const wrapper = document.createElement('div');
wrapper.className =
'chat-inline-card chat-inline-card--html chat-inline-card--iframe chat-inline-html chat-inline-html--iframe';
const iframe = document.createElement('iframe');
iframe.className = 'chat-inline-html__iframe';
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.setAttribute('referrerpolicy', 'no-referrer');
iframe.setAttribute('loading', 'lazy');
wrapper.appendChild(iframe);
persistent = {
encoded: '',
ratioKey: '1:1',
widthPx: 0,
heightPx: 0,
srcdoc: '',
wrapper,
iframe
};
showHtmlJsIframeRenderByPath.set(pathKey, persistent);
}
persistent.wrapper.style.width = `${widthPx}px`;
persistent.iframe.style.height = `${heightPx}px`;
const needsUpdate =
persistent.encoded !== effectiveEncoded ||
persistent.widthPx !== widthPx ||
persistent.heightPx !== heightPx ||
persistent.ratioKey !== ratioKey ||
persistent.srcdoc !== srcdoc;
if (needsUpdate) {
persistent.iframe.srcdoc = srcdoc;
persistent.encoded = effectiveEncoded;
persistent.widthPx = widthPx;
persistent.heightPx = heightPx;
persistent.ratioKey = ratioKey;
persistent.srcdoc = srcdoc;
}
if (!inStreaming || isPartial || needsUpdate || !node.hasAttribute('data-rendered')) {
node.replaceChildren(persistent.wrapper);
}
node.setAttribute('data-rendered', '1');
showHtmlJsPendingRenderByPath.delete(pathKey);
debugShowHtmlLog('render:node-js-iframe', {
renderId,
pathKey,
ratioKey,
widthPx,
heightPx,
encodedLength: effectiveEncoded.length,
srcdocLength: srcdoc.length,
needsUpdate
});
return;
}
if (inStreaming) {
const now = Date.now();
const lastTs = showHtmlStreamingRenderTsByPath.get(pathKey) || 0;
if (now - lastTs < SHOW_HTML_STREAMING_RENDER_INTERVAL_MS) {
const persistent = showHtmlPersistentRenderByPath.get(pathKey);
if (persistent) {
persistent.wrapper.style.width = `${widthPx}px`;
persistent.host.style.height = `${heightPx}px`;
node.replaceChildren(persistent.wrapper);
node.setAttribute('data-rendered', '1');
debugShowHtmlLog('render:node-throttled-rehydrate', {
renderId,
pathKey,
ratioKey,
widthPx,
heightPx
});
}
debugShowHtmlLog('render:node-throttled', {
renderId,
pathKey,
now,
lastTs,
interval: SHOW_HTML_STREAMING_RENDER_INTERVAL_MS
});
return;
}
showHtmlStreamingRenderTsByPath.set(pathKey, now);
} else {
showHtmlStreamingRenderTsByPath.delete(pathKey);
}
const rawHtml = effectiveEncoded ? decodeBase64Utf8(effectiveEncoded) : node.innerHTML || '';
const safeHtml = sanitizeShowHtmlContent(rawHtml);
debugShowHtmlLog('render:node-content', {
renderId,
pathKey,
jsMode,
encodedLength: encoded.length,
effectiveEncodedLength: effectiveEncoded.length,
rawLength: rawHtml.length,
safeLength: safeHtml.length,
ratioKey,
widthPx,
heightPx
});
let persistent = showHtmlPersistentRenderByPath.get(pathKey);
if (!persistent && effectiveEncoded && inStreaming) {
// 精确匹配
let ep = showHtmlPersistentByEncoded.get(effectiveEncoded);
// 前缀匹配:多条渲染源可能输出不同阶段的 encoded用最长前缀匹配找到 COMPLETE 的 wrapper
if (!ep) {
let bestLen = 0;
for (const [key, val] of showHtmlPersistentByEncoded) {
if (key.startsWith(effectiveEncoded) && key.length > bestLen) {
ep = val; bestLen = key.length;
}
}
}
if (ep) {
persistent = { encoded: ep.encoded, ratioKey: ep.ratioKey, widthPx: ep.widthPx, heightPx: ep.heightPx, wrapper: ep.wrapper, host: ep.host, root: ep.root };
showHtmlPersistentRenderByPath.set(pathKey, persistent);
}
}
if (!persistent) {
const wrapper = document.createElement('div');
wrapper.className = 'chat-inline-card chat-inline-card--html chat-inline-html';
const host = document.createElement('div');
host.className = 'chat-inline-card__body chat-inline-html__host';
const shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host {
display: block;
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: auto;
background: transparent;
color: inherit;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
}
*, *::before, *::after {
box-sizing: border-box;
}
.show-html-root {
width: 100%;
height: 100%;
min-height: 100%;
overflow: auto;
background: transparent;
scrollbar-width: thin;
scrollbar-color: rgba(121, 109, 94, 0.48) transparent;
}
.show-html-root::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.show-html-root::-webkit-scrollbar-track {
background: transparent;
}
.show-html-root::-webkit-scrollbar-thumb {
background: rgba(121, 109, 94, 0.48);
border-radius: 8px;
}
.show-html-root::-webkit-scrollbar-thumb:hover {
background: rgba(121, 109, 94, 0.62);
}
`;
const root = document.createElement('div');
root.className = 'show-html-root';
shadow.appendChild(style);
shadow.appendChild(root);
wrapper.appendChild(host);
persistent = {
encoded: '',
ratioKey: '1:1',
widthPx: 0,
heightPx: 0,
wrapper,
host,
root
};
showHtmlPersistentRenderByPath.set(pathKey, persistent);
}
persistent.wrapper.style.width = `${widthPx}px`;
persistent.host.style.height = `${heightPx}px`;
const needsUpdate =
persistent.encoded !== effectiveEncoded ||
persistent.widthPx !== widthPx ||
persistent.heightPx !== heightPx ||
persistent.ratioKey !== ratioKey;
if (needsUpdate) {
persistent.root.innerHTML = safeHtml;
persistent.encoded = effectiveEncoded;
persistent.widthPx = widthPx;
persistent.heightPx = heightPx;
persistent.ratioKey = ratioKey;
if (effectiveEncoded && inStreaming) {
showHtmlPersistentByEncoded.set(effectiveEncoded, {
wrapper: persistent.wrapper, host: persistent.host, root: persistent.root,
encoded: effectiveEncoded, ratioKey, widthPx, heightPx
});
}
debugShowHtmlLog('render:node-persistent-update', {
renderId,
pathKey,
encodedLength: effectiveEncoded.length,
ratioKey,
widthPx,
heightPx
});
} else {
debugShowHtmlLog('render:node-persistent-reuse', {
renderId,
pathKey,
encodedLength: effectiveEncoded.length
});
}
if (!inStreaming || isPartial || needsUpdate || !node.hasAttribute('data-rendered')) {
node.replaceChildren(persistent.wrapper);
}
node.setAttribute('data-rendered', '1');
const rect = persistent.wrapper.getBoundingClientRect();
debugShowHtmlLog('render:node-done', {
renderId,
pathKey,
jsMode,
ratioKey,
widthPx,
heightPx,
rect: {
x: Math.round(rect.x),
y: Math.round(rect.y),
w: Math.round(rect.width),
h: Math.round(rect.height)
},
partial: node.getAttribute('data-partial') || '',
inStreaming
});
if (verbose) {
debugShowImageLog('render:show-html-done', {
renderId,
ratioKey,
widthPx,
heightPx,
contentLength: rawHtml.length
});
}
});
debugShowImageLog('render:end', {
renderId,
orderAfter: collectShowImageOrder(root)
});
}
let showImageObserver: MutationObserver | null = null;
let showContainerObserver: MutationObserver | null = null;
let showTagRenderScheduled = false;
let showTagBindRetryTimer: number | null = null;
let showTagObservedContainer: Element | null = null;
const showHtmlStreamingRenderTsByPath = new Map<string, number>();
const SHOW_HTML_STREAMING_RENDER_INTERVAL_MS = 180;
const showHtmlCompletedSnapshotByPath = new Map<
string,
{
encoded: string,
ratioKey: string
}
>();
const showHtmlStreamingSizeLockByPath = new Map<
string,
{
ratioKey: string,
widthPx: number,
heightPx: number
}
>();
const showHtmlPersistentRenderByPath = new Map<
string,
{
encoded: string,
ratioKey: string,
widthPx: number,
heightPx: number,
wrapper: HTMLElement,
host: HTMLElement,
root: HTMLElement
}
>();
const showHtmlPersistentByEncoded = new Map<string, {
wrapper: HTMLElement, host: HTMLElement, root: HTMLElement,
encoded: string, ratioKey: string, widthPx: number, heightPx: number
}>();
const showHtmlJsPendingRenderByPath = new Map<
string,
{
wrapper: HTMLElement,
host: HTMLElement,
tip: HTMLElement,
widthPx: number,
heightPx: number,
ratioKey: string
}
>();
const showHtmlJsIframeRenderByPath = new Map<
string,
{
encoded: string,
ratioKey: string,
widthPx: number,
heightPx: number,
srcdoc: string,
wrapper: HTMLElement,
iframe: HTMLIFrameElement
}
>();
let layoutDebugObserver: MutationObserver | null = null;
let layoutDebugResizeObserver: ResizeObserver | null = null;
let layoutDebugStarted = false;
let layoutDebugBindRetryTimer: number | null = null;
let layoutDebugCount = 0;
const LAYOUT_DEBUG_MAX = 300;
const layoutDebugLastTsByKey = new Map<string, number>();
function scheduleShowTagRender(container: Element) {
if (showTagRenderScheduled) return;
showTagRenderScheduled = true;
const run = () => {
showTagRenderScheduled = false;
renderShowImages(container);
};
if (typeof queueMicrotask === 'function') {
queueMicrotask(run);
return;
}
Promise.resolve().then(run);
}
function getShowTagContainer() {
return document.querySelector('.messages-area');
}
function isLayoutDebugEnabled() {
if (typeof window === 'undefined') return true;
try {
const explicitFlag = (window as any).__LAYOUT_DEBUG__;
if (explicitFlag === false || explicitFlag === '0') return false;
if (explicitFlag === true || explicitFlag === '1') return true;
const localFlag = window.localStorage?.getItem('layoutDebug');
if (localFlag === '0' || localFlag === 'false') return false;
if (localFlag === '1' || localFlag === 'true') return true;
return true;
} catch {
return true;
}
}
function layoutDebugLog(event: string, payload: Record<string, any> = {}, key = event) {
if (!isLayoutDebugEnabled()) return;
if (layoutDebugCount >= LAYOUT_DEBUG_MAX) return;
const now = Date.now();
const lastTs = layoutDebugLastTsByKey.get(key) || 0;
if (now - lastTs < 120) return;
layoutDebugLastTsByKey.set(key, now);
layoutDebugCount += 1;
if (layoutDebugCount === LAYOUT_DEBUG_MAX) {
console.warn('[LAYOUT_DEBUG]', 'log-limit-reached', { max: LAYOUT_DEBUG_MAX });
return;
}
console.log('[LAYOUT_DEBUG]', event, payload);
}
function elementRectSummary(el: Element | null) {
if (!el) return null;
const rect = el.getBoundingClientRect();
return {
className: (el as HTMLElement).className || '',
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height)
};
}
function setupLayoutDebugObservers() {
if (layoutDebugStarted || !isLayoutDebugEnabled()) return;
const trackedSelectors = [
'.main-container',
'.workspace-region',
'.workspace-panel',
'.chat-container',
'.messages-area'
];
const tracked = trackedSelectors
.map((selector) => document.querySelector(selector))
.filter((el): el is Element => !!el);
if (tracked.length === 0) {
layoutDebugLog('layout:setup-wait', { reason: 'targets-not-mounted' }, 'layout:setup-wait');
if (layoutDebugBindRetryTimer) {
window.clearTimeout(layoutDebugBindRetryTimer);
}
layoutDebugBindRetryTimer = window.setTimeout(() => {
layoutDebugBindRetryTimer = null;
setupLayoutDebugObservers();
}, 250);
return;
}
layoutDebugStarted = true;
layoutDebugLog('layout:setup', {
trackedCount: tracked.length,
tracked: tracked.map((el) => (el as HTMLElement).className || el.tagName.toLowerCase())
});
const snapshot = () => {
layoutDebugLog(
'layout:snapshot',
{
window: {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
},
main: elementRectSummary(document.querySelector('.main-container')),
workspace: elementRectSummary(document.querySelector('.workspace-region')),
panel: elementRectSummary(document.querySelector('.workspace-panel')),
chat: elementRectSummary(document.querySelector('.chat-container')),
messages: elementRectSummary(document.querySelector('.messages-area'))
},
'layout:snapshot'
);
};
snapshot();
layoutDebugObserver = new MutationObserver((mutations) => {
const interesting = mutations.filter((m) => {
if (!(m.target instanceof Element)) return false;
const className = (m.target as HTMLElement).className || '';
return (
m.type === 'attributes' &&
(tracked.some((el) => el === m.target) ||
className.includes('chat-container') ||
className.includes('workspace-panel') ||
className.includes('main-container'))
);
});
if (!interesting.length) return;
layoutDebugLog('layout:mutation', {
count: interesting.length,
items: interesting.slice(0, 8).map((m) => ({
attr: m.attributeName,
target: summarizeNodeForDebug(m.target)
}))
});
snapshot();
});
layoutDebugObserver.observe(document.body, {
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
});
layoutDebugResizeObserver = new ResizeObserver((entries) => {
if (!entries.length) return;
layoutDebugLog('layout:resize', {
items: entries.slice(0, 8).map((entry) => ({
target: summarizeNodeForDebug(entry.target),
width: Math.round(entry.contentRect.width),
height: Math.round(entry.contentRect.height)
}))
});
snapshot();
});
tracked.forEach((el) => layoutDebugResizeObserver?.observe(el));
window.addEventListener('resize', snapshot);
}
function buildNodePathKey(node: Element) {
const getIndexWithin = (el: Element, selector: string) => {
const list = Array.from(document.querySelectorAll(selector));
const idx = list.indexOf(el);
return idx >= 0 ? idx : -1;
};
const parts: string[] = [];
const messageBlock = node.closest('.message-block');
if (messageBlock) {
parts.push(`msg:${getIndexWithin(messageBlock, '.message-block')}`);
}
const actionItem = node.closest('.action-item');
if (actionItem) {
parts.push(`action:${getIndexWithin(actionItem, '.action-item')}`);
}
const textContent = node.closest('.text-content');
if (textContent) {
parts.push(`text:${getIndexWithin(textContent, '.text-content')}`);
}
let cur: Element | null = node;
while (cur && cur !== document.body && parts.length < 12) {
const parent = cur.parentElement;
if (!parent) break;
const index = Array.prototype.indexOf.call(parent.children, cur);
parts.push(`${cur.tagName.toLowerCase()}:${index}`);
if (cur.classList.contains('text-content')) break;
cur = parent;
}
return parts.reverse().join('/');
}
export function setupShowImageObserver() {
if (showImageObserver || showContainerObserver) return;
setupLayoutDebugObservers();
const bind = () => {
const container = getShowTagContainer();
if (!container) {
if (showTagBindRetryTimer) {
window.clearTimeout(showTagBindRetryTimer);
}
showTagBindRetryTimer = window.setTimeout(bind, 250);
return;
}
if (showTagBindRetryTimer) {
window.clearTimeout(showTagBindRetryTimer);
showTagBindRetryTimer = null;
}
if (showTagObservedContainer === container && showImageObserver) return;
if (showImageObserver) {
showImageObserver.disconnect();
showImageObserver = null;
}
showTagObservedContainer = container;
debugShowImageLog('observer:setup', {
container: summarizeNodeForDebug(container),
debugMode: getShowImageDebugMode(),
version: SHOW_IMAGE_DEBUG_VERSION
});
renderShowImages(container);
showImageObserver = new MutationObserver((mutations) => {
const debugMode = getShowImageDebugMode();
const verbose = debugMode === 'verbose';
const hasShowImageRelatedMutation = mutations.some((mutation) => {
const targetIsShowImage =
mutation.target instanceof Element &&
['show_image', 'show_html', 'show-image', 'show-html'].includes(
mutation.target.tagName.toLowerCase()
);
const addedHasShowTag = Array.from(mutation.addedNodes).some(
(n) =>
n instanceof Element &&
(['show_image', 'show_html', 'show-image', 'show-html'].includes(
n.tagName.toLowerCase()
) || !!n.querySelector?.('show_image,show_html,show-image,show-html'))
);
return targetIsShowImage || addedHasShowTag;
});
if (!hasShowImageRelatedMutation) return;
debugShowImageLog('observer:show-image-mutation', {
mutationCount: mutations.length,
mutations: verbose
? mutations.slice(0, 20).map((m) => ({
type: m.type,
target: summarizeNodeForDebug(m.target),
added: Array.from(m.addedNodes)
.slice(0, 6)
.map((n) => summarizeNodeForDebug(n)),
removed: Array.from(m.removedNodes)
.slice(0, 6)
.map((n) => summarizeNodeForDebug(n))
}))
: undefined
});
scheduleShowTagRender(container);
});
showImageObserver.observe(container, { childList: true, subtree: true });
};
bind();
showContainerObserver = new MutationObserver(() => {
const latest = getShowTagContainer();
if (!latest) return;
if (latest !== showTagObservedContainer) {
bind();
}
});
showContainerObserver.observe(document.body, { childList: true, subtree: true });
}
export function teardownShowImageObserver() {
if (showTagBindRetryTimer) {
window.clearTimeout(showTagBindRetryTimer);
showTagBindRetryTimer = null;
}
if (showImageObserver) {
showImageObserver.disconnect();
showImageObserver = null;
}
if (showContainerObserver) {
showContainerObserver.disconnect();
showContainerObserver = null;
}
if (layoutDebugObserver) {
layoutDebugObserver.disconnect();
layoutDebugObserver = null;
}
if (layoutDebugResizeObserver) {
layoutDebugResizeObserver.disconnect();
layoutDebugResizeObserver = null;
}
if (layoutDebugBindRetryTimer) {
window.clearTimeout(layoutDebugBindRetryTimer);
layoutDebugBindRetryTimer = null;
}
layoutDebugStarted = false;
layoutDebugCount = 0;
layoutDebugLastTsByKey.clear();
showHtmlStreamingRenderTsByPath.clear();
showHtmlCompletedSnapshotByPath.clear();
showHtmlStreamingSizeLockByPath.clear();
showHtmlPersistentRenderByPath.clear();
showHtmlPersistentByEncoded.clear();
showHtmlJsPendingRenderByPath.clear();
showHtmlJsIframeRenderByPath.clear();
showTagObservedContainer = null;
}
function updateViewportHeightVar() {
const docEl = document.documentElement;
const visualViewport = window.visualViewport;
if (visualViewport) {
const vh = visualViewport.height;
const bottomInset = Math.max(
0,
(window.innerHeight || docEl.clientHeight || vh) -
visualViewport.height -
visualViewport.offsetTop
);
docEl.style.setProperty('--app-viewport', `${vh}px`);
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);
} else {
const height = window.innerHeight || docEl.clientHeight;
if (height) {
docEl.style.setProperty('--app-viewport', `${height}px`);
}
docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)');
}
}
if (typeof window !== 'undefined') {
window.katex = katex;
updateViewportHeightVar();
window.addEventListener('resize', updateViewportHeightVar);
window.addEventListener('orientationchange', updateViewportHeightVar);
window.addEventListener('pageshow', updateViewportHeightVar);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', updateViewportHeightVar);
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
}
}