35 файлов и одна кнопка, или как вытащить проект из веб-чата нейросети

—

от автора

Некоторые веб-чаты вроде 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

Успех: stories.zip со ссылкой на скачивание

Сработал 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/