
Некоторые веб-чаты вроде DeepSeek и Qwen не умеют отдавать бинарные файлы для скачивания. Я проверил это на практике и написал промпт, который генерирует единый HTML со встроенным ZIP-генератором на чистом JavaScript. Пользователь открывает файл в браузере офлайн, нажимает кнопку — и получает zip-архив со всей структурой директорий и файлов. Дальше его можно поместить под git, развернуть на машине или сделать что угодно ещё. Для нетерпеливых — ссылка на промпт.
Зачем это понадобилось
Идея не всегда приходит в удобный момент. Часто — на ходу: в очереди, на прогулке, пока ждёшь жену в машине или ребёнка с тренировки. Я привык проработку подобных идей начинать на смартфоне в DeepSeek или Qwen с помощью мобильного приложения — быстро изложить верхнеуровневую концепцию, задать ряд вопросов, сформулировать гипотезы, пока мысль не успела раствориться. Если идея вырастает во что-то большее — появляется проект, который переезжает на отдельную виртуальную машину с opencode, где уже существует свой отдельный воркфлоу. Иногда, пока opencode пережёвывает основные проекты, я возвращаюсь к DeepSeek и Qwen, чтобы параллельно обсудить гипотезы и получить обратную связь. И это опять может сгенерировать очередной набор артефактов.
Недавно занимался оптимизацией промптов и конфигурации для opencode. Структура — агенты, скиллы, темплейты, директивы — всё это вылилось в 35 файлов. Принимать участие в чемпионате по спортивной копипасте не хотелось, и тогда созрело решение.
Как это устроено
Промпт генерирует единый HTML-файл с кратким описанием проекта и кнопкой «Download ZIP». Внутри — объект FILES, где ключ — путь внутри архива, значение — полное содержимое файла. Пустые директории передаются через .gitkeep. ZIP собирается на чистом JS: local file headers, central directory, end of central directory record, method store, CRC32 через lookup-таблицу. Никаких JSZip и прочих библиотек. UTF-8, general purpose bit 11 = 1 — чтобы имена читались корректно в 7-Zip, WinRAR, macOS Archive Utility и busybox unzip.
А почему не сгенерировать zip сразу?
Я отправил в Qwen, DeepSeek и Perplexity один и тот же промпт с просьбой сгенерировать zip с двумя небольшими текстовыми файлами. Результаты:
|
Модель |
Ответ |
Примечание |
|---|---|---|
|
DeepSeek |
«I can’t generate actual downloadable files or ZIP archives» |
Предложил текст для копирования вручную |
|
Qwen |
Нет технической возможности отдать бинарный файл |
Нет доступа к файловому хранилищу |
|
Perplexity |
Успех: |
Сработал Perplexity Labs с песочницей и код-исполнением |
Результат от модели к модели может быть разным: одна отказывается, другая уже умеет. Но есть нюанс. По моим наблюдениям, у DeepSeek глубина проработки больше, чем у Perplexity. Для генерации архива Perplexity подходит, а для вдумчивого обсуждения идеи мне больше подходит DeepSeek. Поэтому HTML-трюк остаётся полезным: он позволяет получать артефакты именно от той модели, с которой комфортно взаимодействовать.
Про консистентность
Протестировал пока только в DeepSeek. Четыре раза запустил промпт, получил четыре HTML, распаковал каждый zip в новую директорию и сравнил через diff -r. Вся структура и содержимое совпадают от директории к директории. Разница в объёме html-файлов связана с изменениями, внесёнными мной в промпт и, как следствие, в интерфейс получаемого html-файла (добавилось дерево проекта, описание файлов и их размеры).
$ ls -lahитого 564Kdrwxrwxr-x 6 evgeniy evgeniy 4,0K окт 26 13:02 .drwxrwxr-x 22 evgeniy evgeniy 4,0K окт 26 10:59 ..-rw-rw-r-- 1 evgeniy evgeniy 83K окт 26 10:59 deepseek_html_0.html-rw-rw-r-- 1 evgeniy evgeniy 83K окт 26 12:20 deepseek_html_1.html-rw-rw-r-- 1 evgeniy evgeniy 91K окт 26 12:31 deepseek_html_2.html-rw-rw-r-- 1 evgeniy evgeniy 91K окт 26 12:56 deepseek_html_3.htmldrwxrwxr-x 5 evgeniy evgeniy 4,0K окт 26 11:36 opencode-config-0-rw-rw-r-- 1 evgeniy evgeniy 42K окт 26 10:59 opencode-config-0.2.0_0.zip-rw-rw-r-- 1 evgeniy evgeniy 42K окт 26 12:21 opencode-config-0.2.0_1.zip-rw-rw-r-- 1 evgeniy evgeniy 42K окт 26 12:34 opencode-config-0.2.0_2.zip-rw-rw-r-- 1 evgeniy evgeniy 42K окт 26 12:56 opencode-config-0.2.0_3.zipdrwxrwxr-x 5 evgeniy evgeniy 4,0K окт 26 12:21 opencode-config-1drwxrwxr-x 5 evgeniy evgeniy 4,0K окт 26 12:34 opencode-config-2drwxrwxr-x 5 evgeniy evgeniy 4,0K окт 26 12:57 opencode-config-3-rw-rw-r-- 1 evgeniy evgeniy 2,7K окт 26 12:23 prompt-1.md-rw-rw-r-- 1 evgeniy evgeniy 2,4K окт 26 12:26 prompt-2.md-rw-rw-r-- 1 evgeniy evgeniy 1,7K окт 26 12:49 prompt-3.md$ diff -r opencode-config-0 opencode-config-1$ diff -r opencode-config-0 opencode-config-2$ diff -r opencode-config-0 opencode-config-3$
Предстоит ещё проверить в Qwen, Kimi и других, но текущий результат обнадёживает.
Бонусы, о которых не думал сразу
Имя zip-архива не всегда достаточно, чтобы понять, что находится внутри, особенно если имя содержит название проекта. Чтобы разобраться, нужно открыть архив, заглянуть как минимум в README.md, иногда и в код. На телефоне это ещё менее удобно, чем на компьютере. В то же время html-файл откроется практически любым браузером на любом устройстве и сразу будет содержать описание проекта, извлечённое из контекста общения с нейросетью.
Почему HTML остаётся лучшим выбором
Мой сценарий — «пользователь работает в веб-чате, модель уже знает структуру, нужно отдать всё одним архивом». В этом сценарии html-файл с встроенным zip:
-
работает там, где модель не умеет генерировать бинарные файлы (DeepSeek и Qwen на момент проверки через web-чат);
-
не заставляет переключаться на другую модель только ради архива (Perplexity может сгенерировать zip-архив, но работать с ним мне нравится меньше);
-
не требует установки расширений или доступа к файловой системе;
-
даёт пользователю полный контроль над структурой: иерархия, пустые директории, метаданные.
Про ограничения
DeepSeek, например, позволяет по кнопке продолжать генерацию, что позволяет собрать единый большой html-файл в несколько итераций. Но не факт, что все аналогичные решения дадут такую возможность. В противном случае единый файл, доступный для скачивания, может не собраться. Впрочем, сомнительно, что таким образом кто-то рассчитывает генерировать мегабайты контента. В крайнем случае можно попросить разбить на несколько архивов. Россыпь небольших файлов — наиболее вероятный сценарий, который мы как раз и закрываем текущим решением. Если же проект разрастётся до чего-то тяжёлого, с ним в любом случае нужно работать с помощью предпочитаемого вами harness.
Промпт
Ниже привожу промпт, который получился в результате нескольких итераций доработок. Предложения по доработке и конструктивная критика приветствуются.
Содержимое промпта
Generate a single self-contained HTML file (opencode-config-bundle.html)that lets the user download the opencode-config archive from a phone.Layout, top to bottom:1. Sticky header: - left: title "opencode-config bundle" (single line, truncates on narrow screens) - right: download button, compact, same line as title - below the title, small muted line: "v0.2.0 - 35 files" - status line under the button ("Preparing..." / "Done."), only visible after click, small font2. Section "About": what/why/who, 2 short paragraphs.3. Section "How to use": 4 numbered steps, short.4. Section "Directory tree": full tree with ASCII branches, one line per file: name, short description, size (computed from FILES at load). Directories show a role, not a size. Wrapped in a horizontally scrollable <pre>.5. Section "Prior art": two links with one-line notes.6. Section "Disclaimer": same text as README.md.7. Section "Authorship": Evgeniy Shumilov + DeepSeek, contacts.8. <details> with plain file list.9. <script src="jszip"> + handler. On click: build zip, trigger download, show status. If JSZip is undefined, show a clear error.CSS:- Header is position: sticky; top: 0; background: #fff; border-bottom: 1px solid #ddd; padding: 0.6rem 1rem; z-index: 10.- Header flex: title on the left, button on the right, gap 0.5rem.- Button: compact on desktop (padding 0.5rem 0.9rem, font 0.9rem), full-width on very narrow screens via media query below 420px.- Body has padding-top so sticky header does not overlap content.- No emojis, ASCII only in the tree.- Sizes in the tree are computed from FILES, single source of truth.
Как это встроено в рабочий процесс

На работе и дома я использую i3wm, а в качестве лаунчера — rofi. Для небольших промптов, которые нужны регулярно, у меня есть обвязка: каждый промпт — это отдельный shell-скрипт в ~/.config/rofi/scripts/, содержащий имя, описание и текст. Например, pr-step-by-step:
#!/bin/sh#icon diodon#name Step by step prompttext='Guide me step by step. One command or snippet with a set of commands from you - one feedback from me. Do not use Russian comments, UTF icons, or emojis in snippets.'nohup sh -c "printf '%s' '$text' | /usr/bin/xclip -selection clipboard" >/dev/null 2>&1 &
Вызывается rofi, вводится пара символов, например prom…, выбирается нужный промпт, enter — текст уходит в буфер обмена, дальше можно вставлять в чат. Бывает полезно и для opencode, например, когда несмотря на все проверки и базовые промпты локальная модель пытается выйти за пределы рабочей директории.
Скрытый текст
В конфиге rofi добавлен кастомный мод scripts:
configuration { modi: "drun,calc:qalc,scripts:~/.config/rofi/rofiselector,ssh,window,run"; width: 35; show-icons: true; icon-theme: "papirus"; lines: 10; fake-transparency: true; sidebar-mode: true;}@import "/usr/share/rofi/themes/mytheme.rasi"
Сам ~/.config/rofi/rofiselector — скрипт, который формирует список промптов и обрабатывает выбор.
#!/bin/bashROOT_DIR="$HOME/.config/rofi/scripts"STATE_FILE="/tmp/rofi_scripts_current_dir"save_current_dir() { echo "$1" > "$STATE_FILE"}load_current_dir() { if [ -f "$STATE_FILE" ]; then local saved_dir=$(cat "$STATE_FILE") if [[ "$saved_dir" == "$ROOT_DIR"* ]]; then echo "$saved_dir" return fi fi echo "$ROOT_DIR"}get_metadata() { local file_path="$1" local metadata_type="$2" local line=$(grep -m 1 "^[[:space:]]*#$metadata_type" "$file_path" 2>/dev/null) if [ -n "$line" ]; then echo "$line" | sed "s/^[[:space:]]*#$metadata_type[[:space:]]*//" | xargs else echo "" fi}get_dir_icon() { local dir_path="$1" local icon_file="$dir_path/.icon" if [ -f "$icon_file" ]; then local icon=$(cat "$icon_file" | head -1 | xargs) if [ -n "$icon" ]; then echo "$icon" return fi fi echo "folder"}print_items() { local current_dir="$1" if [ "$current_dir" != "$ROOT_DIR" ]; then echo -en "..\0icon\x1fgo-up\n" fi for item in "$current_dir"/*; do [ -e "$item" ] || continue local name=$(basename "$item") if [ -d "$item" ]; then local icon=$(get_dir_icon "$item") echo -en "$name\0icon\x1f$icon\n" fi done for item in "$current_dir"/*; do [ -e "$item" ] || continue local name=$(basename "$item") if [ -x "$item" ] && [ -f "$item" ]; then local custom_name=$(get_metadata "$item" "name") local icon=$(get_metadata "$item" "icon") if [ -n "$custom_name" ]; then local display_name="$custom_name" else display_name="${name%.*}" fi if [ -n "$icon" ]; then echo -en "$display_name\0icon\x1f$icon\n" else echo -en "$display_name\0icon\x1fterminal\n" fi fi done}run_item() { local current_dir="$1" local selected="$2" if [ "$selected" = ".." ]; then local parent_dir=$(dirname "$current_dir") save_current_dir "$parent_dir" print_items "$parent_dir" return fi for item in "$current_dir"/*; do [ -e "$item" ] || continue local name=$(basename "$item") if [ -d "$item" ] && [ "$name" = "$selected" ]; then local target_dir="$current_dir/$name" if [ -d "$target_dir" ]; then save_current_dir "$target_dir" print_items "$target_dir" fi return fi done for script in "$current_dir"/*; do if [ -x "$script" ] && [ -f "$script" ]; then local script_name=$(basename "$script") local custom_name=$(get_metadata "$script" "name") local file_base="${script_name%.*}" if [ "$selected" = "$custom_name" ] || [ "$selected" = "$file_base" ]; then rm -f "$STATE_FILE" exec "$script" exit 0 fi fi done}if [ -z "$1" ]; then CURRENT_DIR=$(load_current_dir) print_items "$CURRENT_DIR"else CURRENT_DIR=$(load_current_dir) run_item "$CURRENT_DIR" "$1"fi
Итог
Вставил промпт → скачал HTML → открыл → нажал кнопку → получил zip → хоть заноси под git, хоть отправляй в бэкапы. Множество файлов, одна кнопка, ноль копипасты.
ссылка на оригинал статьи https://habr.com/ru/articles/1099452/