Руководство по блокам HTML Preview
Как отображать HTML-контент inline с помощью блоков html-preview и готовить HTML-файлы из разных источников через transform_data.
Обзор
Блок html-preview отображает HTML-файлы в изолированных iframe — подходит для писем, рассылок, HTML-отчётов и любого контента, где конвертация в markdown приведёт к потере оформления.
| Формат | Лучше всего для | Отображение |
|---|---|---|
| Markdown | Текстоёмкий контент, код, списки | Нативный markdown |
Блок html-preview | Письма, рассылки, стилизованные отчёты, богатый HTML | Изолированный iframe с полным CSS |
Ключевой принцип: HTML-контент всегда привязан к файлу (ссылается через src), чтобы не вставлять большие HTML-нагрузки как токены. Типичное HTML-тело письма — 50–150 КБ — его нельзя вставлять напрямую.
Когда использовать
Используйте html-preview, если:
- HTML-тела писем — Gmail, Outlook или любой email API возвращает HTML-контент
- Рассылки — Substack, Mailchimp и другие сервисы используют сложные CSS-макеты, которые markdown не воспроизводит
- HTML-отчёты — ответы API с уже отформатированным HTML (аналитические дашборды, сгенерированные отчёты)
- Богатые документы — любой контент со сложным CSS, табличными макетами, фоновыми изображениями или кастомными шрифтами
- Веб-контент — HTML-снимки или превью, где важна точность макета
Не используйте html-preview, если:
- Контент — простой текст — просто выведите его в markdown
- Контент — структурированные данные — используйте
datatableилиspreadsheet - Контент — фрагмент кода — используйте обычные блоки кода с подсветкой синтаксиса
- HTML очень маленький (< 1 КБ) — вместо этого сделайте резюме в markdown
Базовое использование
Один элемент
```html-preview
{
"src": "/absolute/path/to/file.html",
"title": "My HTML Content"
}
```Несколько элементов (вкладки)
Если есть несколько связанных HTML-файлов (например, цепочка писем, несколько отчётов), используйте массив items. Под заголовком появится панель вкладок для переключения между элементами.
```html-preview
{
"title": "Email Thread",
"items": [
{ "src": "/path/to/original.html", "label": "Original" },
{ "src": "/path/to/reply.html", "label": "Reply" },
{ "src": "/path/to/forward.html", "label": "Forward" }
]
}
```Контент загружается лениво при переключении вкладки и кэшируется после загрузки.
Поля конфигурации
| Поле | Обязательно | Тип | Описание |
|---|---|---|---|
src | Да* | string | Абсолютный путь к HTML-файлу на диске (режим одного элемента) |
title | Нет | string | Отображаемый заголовок в верхней панели (по умолчанию "HTML Preview") |
items | Да* | array | Массив элементов с src и необязательным label (режим нескольких элементов) |
items[].src | Да | string | Абсолютный путь к HTML-файлу |
items[].label | Нет | string | Подпись вкладки (по умолчанию "Item 1", "Item 2" и т. д.) |
*Требуется либо src (один), либо items (несколько). Если оба присутствуют, приоритет у items.
Важно: путь src должен быть абсолютным — используйте точный путь, возвращённый transform_data, или соберите его с использованием пути к папке данных сессии.
Подготовка HTML-контента
Использование transform_data
Инструмент transform_data — основной способ извлекать и записывать HTML-файлы. Он запускает скрипт, который читает входные файлы и записывает результат.
Ключевое отличие от использования datatable: для html-preview выходной файл — .html (не .json). Скрипт записывает исходный HTML-контент, а не JSON.
Параметры:
| Параметр | Тип | Описание |
|---|---|---|
language | "python3" | "node" | "bun" | Среда выполнения скрипта |
script | string | Исходный код скрипта преобразования |
inputFiles | string[] | Пути входных файлов относительно папки сессии |
outputFile | string | Имя выходного файла, заканчивающееся на .html (записывается в папку data/ сессии) |
Соглашения по путям:
- Входные файлы — относительно папки сессии. Частые расположения:
long_responses/tool_result_abc.txt— сохранённые результаты инструментов (ответы Gmail API и т. п.)data/previous_output.html— результат предыдущего преобразования
- Выходной файл — относительно папки
data/сессии. Достаточно указать имя файла (например,"email.html")
Использование инструмента Write
Для небольших HTML-контента (сгенерированные отчёты, простой HTML) можно напрямую использовать инструмент Write, чтобы записать .html файл в папку данных сессии, а затем сослаться на него.
Частые паттерны и рецепты
Отображение писем Gmail
Gmail API возвращает тела писем как строки, закодированные в base64url. HTML-тело обычно находится в payload.parts[1].body.data для multipart-писем.
Надёжный паттерн (обрабатывает все MIME-структуры):
import base64, json, sys
with open(sys.argv[1]) as f:
msg = json.load(f)
# Рекурсивно найти часть text/html в MIME-структуре
def find_html_part(payload):
if payload.get('mimeType') == 'text/html':
return payload.get('body', {}).get('data')
for part in payload.get('parts', []):
result = find_html_part(part)
if result:
return result
return None
html_b64 = find_html_part(msg['payload'])
if not html_b64:
# Запасной вариант: само тело может быть HTML (письма без multipart)
html_b64 = msg['payload'].get('body', {}).get('data', '')
# Gmail использует URL-safe base64
html = base64.urlsafe_b64decode(html_b64).decode('utf-8')
with open(sys.argv[-1], 'w') as f:
f.write(html)Вызов:
transform_data({
language: "python3",
script: "...",
inputFiles: ["long_responses/gmail_message.txt"],
outputFile: "email.html"
})Простой вариант (если структура известна):
import base64, json, sys
data = json.load(open(sys.argv[1]))
html = base64.urlsafe_b64decode(data['payload']['parts'][1]['body']['data']).decode('utf-8')
open(sys.argv[-1], 'w').write(html)Письма Microsoft Outlook
Outlook / Microsoft Graph API возвращает тела писем иначе:
import json, sys
with open(sys.argv[1]) as f:
msg = json.load(f)
# Microsoft Graph возвращает HTML в body.content
html = msg.get('body', {}).get('content', '')
with open(sys.argv[-1], 'w') as f:
f.write(html)HTML из ответов API
Многие API возвращают HTML-контент в JSON-поле:
import json, sys
with open(sys.argv[1]) as f:
data = json.load(f)
# Подставьте имя поля под ваш API
html = data.get('html_content', data.get('body_html', data.get('html', '')))
with open(sys.argv[-1], 'w') as f:
f.write(html)Сгенерированный HTML-отчёт
Соберите HTML-отчёт из структурированных данных:
import json, sys
with open(sys.argv[1]) as f:
data = json.load(f)
items = data.get('items', data.get('data', []))
rows_html = ''.join(
f'<tr><td>{item["name"]}</td><td>${item["amount"]:,.2f}</td></tr>'
for item in items
)
html = f"""<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 24px; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ padding: 8px 12px; border-bottom: 1px solid #eee; text-align: left; }}
th {{ font-weight: 600; color: #666; }}
</style>
</head>
<body>
<h2>Report</h2>
<table>
<thead><tr><th>Name</th><th>Amount</th></tr></thead>
<tbody>{rows_html}</tbody>
</table>
</body>
</html>"""
with open(sys.argv[-1], 'w') as f:
f.write(html)Альтернатива на Node.js
const fs = require('fs');
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8'));
// Извлечь HTML из письма Gmail
const html = Buffer.from(data.payload.parts[1].body.data, 'base64url').toString('utf-8');
fs.writeFileSync(process.argv.at(-1), html);Полный пример рабочего процесса
Пользователь спрашивает: «Покажи мне ту рассылку от Scott Belsky»
Шаг 1: Найдите письмо в Gmail:
GET gmail/v1/users/me/messages?q=from:scott belsky subject:implicationsШаг 2: Получите полное сообщение:
GET gmail/v1/users/me/messages/{id}?format=fullШаг 3: Вызовите transform_data, чтобы декодировать HTML-тело:
transform_data({
language: "python3",
script: "import base64, json, sys\nwith open(sys.argv[1]) as f:\n msg = json.load(f)\ndef find_html(p):\n if p.get('mimeType')=='text/html': return p['body']['data']\n for part in p.get('parts',[]): \n r=find_html(part)\n if r: return r\nhtml=base64.urlsafe_b64decode(find_html(msg['payload'])).decode('utf-8')\nopen(sys.argv[-1],'w').write(html)",
inputFiles: ["long_responses/gmail_result.txt"],
outputFile: "newsletter.html"
})Шаг 4: Выведите блок html-preview с абсолютным путём из результата transform_data:
```html-preview
{
"src": "/absolute/path/from/transform_data/newsletter.html",
"title": "Implications #40 — Exponential Code, Network Effects In AI"
}
```Поведение при отображении
Inline-превью
- Фиксированный
max-height400px с нижним градиентом затухания, указывающим на наличие контента ниже - Кнопка развернуть (в правом верхнем углу, видна при наведении) открывает полноэкранный вид
- Верхняя панель показывает иконку Globe и заголовок
Полноэкранный оверлей
- Нажмите кнопку развернуть для отображения на всю высоту с прокручиваемым контентом
- Кнопка Copy HTML копирует исходный HTML в буфер обмена
- Бейдж "HTML" в заголовке указывает тип контента
Визуальные детали
- Белый фон — iframe отображаются с белым фоном (стандарт для HTML-писем/документов)
- Внешние изображения — загружаются из исходных URL (CSP поддерживает
https://) - CSS-стили — работают все inline- и встроенные стили (без ограничений на внешние таблицы стилей)
- Адаптивные макеты — если в HTML есть адаптивный CSS, он подстраивается под ширину iframe
Рекомендации для писем
Поиск HTML-части
MIME-структуры писем различаются. Частые паттерны:
| Структура | Где находится HTML |
|---|---|
multipart/alternative | payload.parts[1].body.data (обычно индекс 1 — HTML) |
multipart/mixed → multipart/alternative | payload.parts[0].parts[1].body.data |
| Одночастный HTML | payload.body.data (нет массива parts) |
| Только текстовое письмо | Нет HTML-части — используйте markdown |
Всегда используйте рекурсивный паттерн find_html_part() из рецепта Gmail выше — он надёжно обрабатывает все структуры.
Base64-кодирование Gmail
Gmail использует URL-safe base64 (RFC 4648 §5):
- Используются
-и_вместо+и/ - Нет выравнивания (
=) - Python:
base64.urlsafe_b64decode()обрабатывает это - Node:
Buffer.from(data, 'base64url')
Не используйте стандартный base64.b64decode() — он не сработает для URL-safe-контента.
Крупные письма
Некоторые HTML-тела рассылок превышают 100 КБ. Это нормально:
transform_dataзаписывает на диск (без затрат токенов)- Iframe загружает файл напрямую
- Inline-превью 400px показывает только верхнюю часть
Безопасность
HTML отображается в изолированном iframe со следующими ограничениями:
| Возможность | Статус | Детали |
|---|---|---|
| Выполнение JavaScript | Заблокировано | Атрибут sandbox без allow-scripts |
| Отправка форм | Заблокировано | Нет allow-forms |
| Навигация по ссылкам | Заблокировано | Sandbox блокирует всю навигацию |
| Всплывающие окна / новые окна | Заблокировано | Нет allow-popups |
| CSS-стили | Разрешено | Работают inline-, встроенные и теги <style> |
Изображения (https://) | Разрешено | Внешние изображения загружаются нормально |
Изображения (data:) | Разрешено | Base64-изображения работают |
| Встроенные шрифты | Разрешено | Загружаются Google Fonts и другие шрифты с CDN |
HTML-санитизация не требуется — атрибут sandbox обеспечивает полную изоляцию на уровне процесса. Злонамеренные скрипты, формы и навигация блокируются на уровне движка браузера.
Лучшие практики
Дерево решений
Содержимое — богатый HTML с важным оформлением/макетом?
→ ДА: используйте html-preview
→ НЕТ: конвертируйте в markdown
HTML-контент большой (> 1 КБ)?
→ ДА: используйте transform_data, чтобы записать файл, и ссылайтесь через src
→ НЕТ: рассмотрите просто резюме в markdown
Пользователь явно хочет УВИДЕТЬ письмо/HTML?
→ ДА: используйте html-preview (важна визуальная точность)
→ НЕТ: извлеките текстовое содержимое и представьте в markdownСоглашения по именам
- Выходные файлы: описательные, kebab-case —
newsletter-jan-2026.html,quarterly-report.html - Совпадение с контекстом — если пользователь спрашивал о конкретном письме, назовите файл по теме письма
Советы по скриптам
- Для декодирования писем предпочитайте Python —
base64иjsonвходят в стандартную библиотеку, зависимости не нужны - Для Gmail всегда используйте
urlsafe_b64decode(никогдаb64decode) - Используйте рекурсивный паттерн
find_html_part()— он обрабатывает все структуры писем - Держите скрипты лаконичными — сложную логику сложнее отлаживать при таймауте 30 секунд
Устранение неполадок
«Loading...» показывается бесконечно
- Путь
"src"должен быть абсолютным — используйте точный путь, возвращённыйtransform_data - Не собирайте относительные пути и не угадывайте расположение папки данных
- Проверьте, что
transform_dataзавершился успешно (посмотрите сообщение результата инструмента)
Пустой/белый iframe
- HTML-файл может быть пустым — проверьте вывод
transform_dataна ошибки - Base64-декодирование могло молча не сработать — убедитесь, что скрипт корректно обрабатывает структуру письма
- Проверьте, есть ли в письме HTML-часть (некоторые письма только текстовые)
Изображения не загружаются
- Изображения с URL
http://могут блокироваться (CSP требуетhttps://) - Некоторые изображения писем используют пиксели отслеживания, срок действия которых мог истечь
- Inline-изображения
cid:(Content-ID) не поддерживаются — им нужны MIME-вложения письма
Нечитаемый текст / проблемы с кодировкой
- Всегда декодируйте в
utf-8:.decode('utf-8') - Некоторые старые письма используют другие кодировки — проверьте заголовок
Content-Typeна наличиеcharset - Если charset не UTF-8, декодируйте соответственно:
.decode(charset)
HTML отображается как исходный код (не рендерится)
- Убедитесь, что язык блока кода —
html-preview(неhtmlилиhtm) - Проверьте, что JSON-конфигурация валидна: должно быть поле
"src" - Убедитесь, что содержимое файла — настоящий HTML (не JSON, содержащий HTML)
Ошибки скрипта в transform_data
KeyErrorнаpayload.parts— письмо может быть одночастным (нет массиваparts). Используйте рекурсивный паттернfind_html_part()binascii.Error: Invalid base64— письмо может использовать стандартный base64, а не URL-safe. Попробуйтеbase64.b64decode()как запасной вариантUnicodeDecodeError— проверьте кодировку charset письма (см. проблемы с кодировкой выше)
Нужен такой агент в вашей компании?
Устанавливаем под ключ: настройка на компьютерах сотрудников, единый аккаунт, интеграция с 1С и внутренними системами, обучение команды. Подробнее о внедрении →