
Некоторые веб‑чаты вроде 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 итого 564K drwxrwxr-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.html drwxrwxr-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.zip drwxrwxr-x 5 evgeniy evgeniy 4,0K окт 26 12:21 opencode-config-1 drwxrwxr-x 5 evgeniy evgeniy 4,0K окт 26 12:34 opencode-config-2 drwxrwxr-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 font 2. 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 prompt text='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/bash ROOT_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, хоть отправляй в бэкапы. Множество файлов, одна кнопка, ноль копипасты.

