- BrainTools - https://www.braintools.ru -

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

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

Некоторые веб‑чаты вроде DeepSeek и Qwen не умеют отдавать бинарные файлы для скачивания. Я проверил это на практике и написал промпт, который генерирует единый HTML со встроенным ZIP‑генератором на чистом JavaScript. Пользователь открывает файл в браузере офлайн, нажимает кнопку — и получает zip‑архив со всей структурой директорий и файлов. Дальше его можно поместить под git, развернуть на машине или сделать что угодно ещё. Для нетерпеливых — ссылка на промпт [1].


Зачем это понадобилось

Идея не всегда приходит в удобный момент. Часто — на ходу: в очереди, на прогулке, пока ждёшь жену в машине или ребёнка с тренировки. Я привык проработку подобных идей начинать на смартфоне в 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 [2] со ссылкой на скачивание

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

Как это встроено в рабочий процесс

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

На работе и дома я использую 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 "..iconx1fgo-upn"
    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 "$nameiconx1f$iconn"
        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_nameiconx1f$iconn"
            else
                echo -en "$display_nameiconx1fterminaln"
            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, хоть отправляй в бэкапы. Множество файлов, одна кнопка, ноль копипасты.

Автор: MasterShu

Источник [3]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/37525

URLs in this post:

[1] ссылка на промпт: #prompt

[2] stories.zip: http://stories.zip

[3] Источник: https://habr.com/ru/articles/1099452/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1099452

www.BrainTools.ru

Rambler's Top100