Развёртывание и расширение¶
Чтобы перенести flower в другое место, нужно решить три вещи: контейнер (обнести неограниченный Bash и заодно проверить фразу «без CLI»), plugin (доменные возможности едут вместе с репозиторием и не зависят от того, что установлено на хосте), сайт документации (push в main — и публикация происходит сама, install.sh висит на домене Pages). Три раздела независимы, читайте по необходимости.
1. Контейнер¶
Зачем нужен контейнер¶
Первое — обнести. У работающего исполнителя неограниченный Bash — белый список Bash во flower (delegate_guard) распространяется только на главный поток, а тот, кого отправили наружу, должен уметь гонять тесты, так что это сделано намеренно. В контейнер монтируется только каталог вашего проекта, исходники фреймворка лежат внутри образа в /opt/flower, остального на хосте не видно.
Второе — сам по себе он и есть проверка ограничения переносимости. В образе нет Claude Code CLI, нет Node, только Python и claude-agent-sdk — запросы уходят через нативный бинарник, который поставляется в wheel. Если здесь всё запускается, то «без CLI» — не декларация на бумаге.
Проверено на практике (2026-09-06, macOS 15 / arm64 / colima + docker 28.4.0):
| Что проверяли | Результат |
|---|---|
| Есть ли CLI в образе | claude, node, npm, npx — ни одного нет |
| Встроенный бинарник | \177ELF (207M) |
| Реальный запрос | отправлен через cloud.infini-ai.com/maas и получен ответ, $0.1741 / 1 раунд (для Opus 5 + окно 1M это и есть нижняя граница одного раунда) |
| Владелец файлов | файл, записанный внутри контейнера в /work, на хосте принадлежит hechenyu:staff — маппинг корректный |
| Видимость хоста | в контейнере ls /Users → No such file or directory |
Что установлено в образе¶
Базовый образ python:3.13-slim, поверх него apt ставит всего три пакета. У каждого есть причина:
| Что ставим | Зачем |
|---|---|
python:3.13-slim | Нужен только Python ≥ 3.10. Ни Node, ни claude CLI |
git | --isolate выдаёт каждому subagent отдельный worktree |
ca-certificates | Ходим через HTTPS-шлюз |
libstdc++6 | Встроенный в SDK бинарник — однофайловая сборка Bun, на Linux ему нужна эта библиотека; в slim-образе её нет |
Исходники фреймворка попадают в образ через COPY, а не bind mount — поэтому агент внутри контейнера не дотянется до исходников фреймворка на хосте:
| Путь в образе | Содержимое | Откуда |
|---|---|---|
/opt/flower | pyproject.toml, flower/, examples/, здесь же выполняется pip install . | COPY |
/work | Рабочий каталог (WORKDIR), в рантайме сюда монтируется $PWD хоста | docker run -v |
Точка входа — ENTRYPOINT ["flower"], CMD пустой: запуск контейнера без аргументов уходит в интерактивный ввод (он спрашивает, что нужно сделать), а не печатает --help. Так не приходится брать в кавычки формулировку задачи в shell.
Почему нельзя примонтировать .venv с хоста¶
SDK публикует wheel по платформам, и встроенный бинарник платформозависим:
Хост claude_agent_sdk-0.2.152-py3-none-macosx_11_0_arm64.whl
→ _bundled/claude — это Mach-O 64-bit arm64, 191M
Контейнер claude_agent_sdk-0.2.152-py3-none-manylinux_2_17_aarch64.whl
Примонтированный он не запустится, поэтому образ обязан делать свой pip install. С другой стороны, это и есть доказательство переносимости: один и тот же pyproject.toml, смена платформы меняет нативный бинарник, а код фреймворка не правится ни на строку.
Два скрипта¶
| Скрипт | Что делает |
|---|---|
docker/build | Собирает образ. cd в корень репозитория, docker build -f docker/Dockerfile -t flower-box .; при FLOWER_MIRRORS=1 (по умолчанию) сначала тянет python:3.13-slim из зеркала registry и делает retag, а также передаёт --build-arg для pip / apt |
docker/flowerbox | Один запуск. Проверяет файл с учётными данными → проверяет, монтируется ли $PWD → определяет наличие TTY → docker run |
Переключатели docker/build — все через переменные окружения:
| Переменная | По умолчанию | Смысл |
|---|---|---|
FLOWER_IMAGE | flower-box | Тег образа |
FLOWER_MIRRORS | 1 | 0 = не подменять ни одного зеркала, всё с upstream |
FLOWER_REGISTRY | dockerproxy.net | Отсюда тянется базовый образ и переименовывается в python:3.13-slim, чтобы FROM попал в локальный |
FLOWER_PIP_INDEX | https://mirrors.aliyun.com/pypi/simple/ | Передаётся в --build-arg PIP_INDEX_URL |
FLOWER_APT_MIRROR | mirrors.ustc.edu.cn | Передаётся в --build-arg APT_MIRROR |
Последние три работают только при FLOWER_MIRRORS=1 — ветка FLOWER_MIRRORS=0 вообще не выставляет build-arg.
docker/flowerbox понимает две:
| Переменная | По умолчанию | Смысл |
|---|---|---|
FLOWER_HOME | Уровнем выше расположения самого скрипта (то есть корень репозитория) | Где искать .env. Если $FLOWER_HOME/.env не найден — сразу выход с кодом 1 |
FLOWER_IMAGE | flower-box | Какой образ запускать |
FLOWER_HOME выводится из положения самого скрипта, путь нигде не зашит, поэтому репозиторий можно клонировать куда угодно.
Собрать образ и поднять контейнер¶
docker/build # достаточно одного раза
cd ~/любой-каталог-проекта # обязательно внутри $HOME, см. границы монтирования ниже
/path/to/flower/docker/flowerbox # без аргументов → он сам спросит, что делать, кавычки не нужны
Если сеть до pypi.org / Docker Hub в порядке, сборка запускается так:
Аргументы flowerbox полностью совпадают с flower — он подставляет "$@" как есть после ENTRYPOINT. --clarify-only, --asks N, --timeout секунды, --isolate, -v передаются напрямую, полная таблица — в командной строке:
cd ~/proj
/path/to/flower/docker/flowerbox --clarify-only -v
/path/to/flower/docker/flowerbox "Сделай мне X"
Реально выполняется вот эта строка (-t добавляется только при наличии TTY, см. ниже):
docker run -i $TTY --rm \
--env-file "$FLOWER_HOME/.env" \
-v "$PWD:/work" \
-w /work \
"$IMAGE" "$@"
Границы монтирования и персистентность¶
$PWD хоста ──монтируется──> /work ← здесь работает агент, результаты остаются на хосте
внутри образа /opt/flower ← исходники фреймворка, **не смонтированы**, до хоста не достать
Поэтому запускаться в подкаталоге репозитория вроде flower/human-test/HT001 тоже безопасно: внутрь монтируется только HT001, исходники фреймворка в область монтирования не попадают.
| Что | Останется после выхода | Почему |
|---|---|---|
Всё под $PWD хоста, включая runs/ и верстак .flower/ | Да | Это и есть тот каталог, который смонтирован как /work |
| Записанное по другим путям внутри контейнера | Нет | --rm, контейнер удаляется при выходе |
| Учётные данные | Не попадают в слои образа | Идут через --env-file; .env исключён в .dockerignore, так что даже COPY . . его не затащит |
Каталог проекта обязан быть внутри $HOME, иначе результаты молча теряются
colima по умолчанию монтирует в VM только $HOME (mount | grep virtiofs → mount0 on /Users/<вы>). Если запускать где-нибудь в /tmp, -v создаст в VM пустой каталог, и записанное туда хост никогда не увидит, причём без ошибки — результаты, бриф, runs/ теряются целиком. Наступали один раз: прогон once отработал, $0.17 потрачены, а runs/ на хосте попросту нет.
Сейчас flowerbox такое отсекает: если $PWD внутри $HOME — пропускает сразу; если нет — пишет в $PWD файл-пробу и поднимает ещё один контейнер, который фактически проверяет test -f /work/<проба> (с дополнительными монтированиями тоже проходит). Не прошло — выход 1 и подсказка colima start --mount '<путь>:w'. Для пробы нужен контейнер, так что сначала docker/build.
Как учётные данные попадают в контейнер¶
Через docker run --env-file, в слои образа они не попадают. flowerbox читает $FLOWER_HOME/.env, по умолчанию это .env в корне репозитория:
Учтите: flower setup пишет в ~/.config/flower/.env, и этот путь flowerbox не смотрит. Если вы уже настроились через setup и не хотите держать вторую копию, направьте туда FLOWER_HOME:
Имена ключей, приоритеты и как заполнять шлюз — см. конфигурацию.
Без TTY вопросы будут висеть до самого --timeout
flowerbox добавляет -t только при [ -t 0 ] — docker run -t в конвейере / CI сразу ругается «the input device is not a TTY»; -i нужен всегда, иначе stdin вообще не пройдёт внутрь.
Ответы на вопросы идут через стандартный ввод. Без TTY input() уже на первом вызове бросает EOFError → текущий вопрос считается «ввод закрыт» и пропускается, а поток ответов сразу завершается, поэтому начиная со второго вопроса отвечать некому — остаётся только ждать до конца --timeout (по умолчанию 1800 секунд). Для автономных запусков нужен явный --timeout 0. Обнаружив отсутствие TTY, скрипт сначала печатает предупреждение.
git submodule¶
В .gitmodules всего одна запись:
| path | url | Что это |
|---|---|---|
human-test/HT001 | https://github.com/ChenyuHeee/cppide.git | Репозиторий кода, произведённый тем прогоном HT001, хранится для истории |
Обычный git clone его не подтянет, и human-test/HT001 останется пустым каталогом (git submodule status покажет - в начале строки — это оно). Нужно ли с этим что-то делать:
| Что вы хотите | Нужна ли инициализация |
|---|---|
| Запускать flower, собирать образ | Нет. human-test/ исключён в .dockerignore, а Dockerfile и так COPY только pyproject.toml / flower / examples |
| Полистать локально код, произведённый в HT001 | Да: git submodule update --init human-test/HT001 либо сразу git clone --recurse-submodules |
Сеть внутри Китая: откуда взялась вся эта пачка подмен зеркал¶
При установке этого хозяйства за файрволом узкое место — не полоса, а международные каналы. Дефолтный docker/build уже подменяет всё, что нужно, а FLOWER_MIRRORS=0 разом всё отключает. Ниже — замеры и происхождение четырёх подмен; если у вас с сетью такой проблемы нет, читать не нужно.
Таблица замеренных скоростей и четыре подмены (2026-09-06, macOS/arm64)
| Источник | Скорость |
|---|---|
pypi.org (индекс) | 32 KB/s |
files.pythonhosted.org (файлы пакетов) | 284 B/s |
github.com (release asset напрямую) | 22 KB/s |
cloud-images.ubuntu.com | 382 B/s |
deb.debian.org | 32 KB/s |
ports.ubuntu.com (внутри VM) | 26 KB/s |
download.docker.com | недоступен (HTTP 000); внутри VM 4 KB/s |
mirrors.tuna.tsinghua.edu.cn | недоступен |
mirrors.aliyun.com/pypi (файлы пакетов) | 1.4 MB/s (хост) / 152 KB/s (внутри VM) |
mirrors.ustc.edu.cn/ubuntu-cloud-images | 28 MB/s |
mirrors.ustc.edu.cn/ubuntu-ports (внутри VM) | 1.95 MB/s |
mirrors.ustc.edu.cn/debian | 435 KB/s |
ghfast.top (прокси GitHub) | 2.5 MB/s |
gh-proxy.com (прокси GitHub) | 1.5 MB/s |
dockerproxy.net (прокси Docker Hub) | Работает (сразу отдаёт manifest) |
При замерах не путайте страницу индекса с файлами пакетов: страница mirrors.aliyun.com/pypi/simple/ выдаёт 7.4 MB/s, а настоящий wheel на 95.9 MB — всего 1.4 MB/s (внутри VM 152 KB/s — пользовательская сеть colima теряет на этом). Оценивайте время по цифрам для файлов пакетов.
Подмена 1 — образ VM для colima. colima использует не обычный Ubuntu cloud image, а свой кастомный образ с предустановленным docker (release asset из abiosoft/colima-core), поэтому при поднятии VM не нужно ставить docker через apt, а значит обходится недоступный download.docker.com. Скачайте его сами и скормите через --disk-image:
A=https://github.com/abiosoft/colima-core/releases/download/v0.9.0-2/ubuntu-24.04-minimal-cloudimg-arm64-docker.qcow2
mkdir -p ~/.colima/images
curl -sSL -C - -o ~/.colima/images/colima-arm64-docker.qcow2 "https://ghfast.top/$A"
# Проверка: digest берём из GitHub API. Не пропускайте — это будет запускаться как VM
curl -sSL https://api.github.com/repos/abiosoft/colima-core/releases/tags/v0.9.0-2 \
| python3 -c "import json,sys;[print(a['digest'],a['name']) for a in json.load(sys.stdin)['assets'] if a['name'].endswith('arm64-docker.qcow2')]"
shasum -a 256 ~/.colima/images/colima-arm64-docker.qcow2
colima start --disk-image ~/.colima/images/colima-arm64-docker.qcow2 \
--cpu 4 --memory 6 --disk 20
Прокси рвёт поток на середине (наблюдался curl 56), -C - докачивает — просто перезапустите несколько раз.
Подмена 2 — apt внутри VM. Даже с образом, где docker предустановлен, boot-скрипт lima 30-install-packages.sh всё равно один раз запустит apt-get update, чтобы поставить rsync — и полезет в ports.ubuntu.com (26 KB/s) и download.docker.com (4 KB/s), залипая на десятки минут.
Что делать (сначала прочитайте /mnt/lima-cidata/boot.sh, потом действуйте: на упавший boot-скрипт он реагирует лишь WARNING + CODE=1 и продолжает, а в конце обязательно пишет /run/lima-boot-done, так что уронить этот шаг безопасно):
export LIMA_HOME=~/.colima/_lima
limactl shell colima -- sudo sh -c '
cat > /etc/apt/sources.list.d/ubuntu.sources <<EOF
Types: deb
URIs: https://mirrors.ustc.edu.cn/ubuntu-ports/
Suites: noble noble-updates noble-backports noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
sed -i "s|https://download.docker.com|https://mirrors.ustc.edu.cn/docker-ce|g" \
/etc/apt/sources.list.d/docker.list
pkill -f "apt-get update" # boot.sh доработает до конца и запишет метку завершения
'
# colima start после этого нормально завершается; затем доставляем rsync (теперь 1.95 MB/s)
limactl shell colima -- sudo sh -c 'apt-get update -q && apt-get install -y -q rsync'
Заодно добавьте 127.0.0.1 lima-colima в /etc/hosts внутри VM, чтобы убрать пачку предупреждений sudo: unable to resolve host.
Подмена 3 — базовый образ. docker/build сначала тянет python:3.13-slim с dockerproxy.net и делает retag, чтобы FROM в Dockerfile попал в локальный образ. По замерам dockerproxy.net сразу отдаёт manifest (HTTP 200); docker.1ms.run / docker.m.daocloud.io отвечают 401, hub.rat.dev — 302, docker.xuanyuan.me — 403.
Подмена 4 — apt и pip внутри контейнера. --build-arg APT_MIRROR=mirrors.ustc.edu.cn (deb.debian.org 32 KB/s → USTC 435 KB/s), --build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/. Обратите внимание: PIP_INDEX_URL одновременно является переменной окружения, которую pip понимает сам, поэтому достаточно объявить ARG, и pip внутри RUN её прочитает — явно писать --index-url не нужно.
Разовая подготовка (при описанных выше условиях сети) занимает примерно 25 минут, львиная доля — образ VM на 364 MB и wheel SDK на 95.9 MB. После этого flowerbox стартует за секунды.
2. plugin¶
Что такое plugin и как SDK его загружает¶
Это пакет доменных возможностей, который едет вместе с репозиторием. Код фреймворка не содержит никаких доменных знаний, они целиком лежат в каталоге plugin/ в корне репозитория и вместе с кодом клонируются, ревьюятся и тегируются.
Со стороны SDK всё подключение — две строки в build_options() в flower/core/agent.py:
PLUGIN_DIR = Path(__file__).resolve().parent.parent.parent / "plugin"
...
if use_plugin and PLUGIN_DIR.is_dir():
opts["plugins"] = [{"type": "local", "path": str(PLUGIN_DIR)}]
В связке с setting_sources=[] (о нём отдельно ниже) именно это позволяет flower одновременно быть переносимым и «понимать вашу предметную область»: он не спрашивает, что установлено на хосте, и знает только один каталог, приехавший вместе с репозиторием.
Раскладка каталогов¶
| Путь | Что лежит | Когда срабатывает | Кто решает |
|---|---|---|---|
plugin/.claude-plugin/plugin.json | Идентичность пакета: name, description, version, author | Читается один раз при загрузке | — |
plugin/skills/<name>/SKILL.md | Доменные знания, подгружаются по необходимости | Вероятностно — используется, если модель сочтёт релевантным | Модель |
plugin/agents/<name>.md | subagent, отдельное окно контекста | Модель делегирует либо явно указано в workflow | Модель / вы |
plugin/hooks/hooks.json | Перехват вызовов инструментов | Детерминированно — совпало, значит выполняется | Код |
plugin/.mcp.json | Подключение внешних инструментов | Регистрируются как инструменты, наравне со встроенными | Модель |
Разница между вероятностным и детерминированным — это ключ к выбору, а не разница формулировок:
- skill — это знание, которое лежит рядом. Модель видит его
descriptionи читает, только если сочтёт релевантным текущей задаче. Оценку релевантности делает модель, поэтому один и тот же запрос в двух прогонах может один раз его использовать, а другой — нет. - hook — это код. Событие совпало — он выполняется, вне зависимости от того, хочет ли модель и знает ли она о нём. Собственные сброс на диск и изоляция во flower — именно hook'и, ровно потому что они не могут работать «иногда».
Значит, критерий один: должно ли это происходить каждый раз? Должно — пишите hook. Просто «знать полезно» — пишите skill. Оформить обязательное как skill — значит поставить дисциплину в зависимость от одного решения модели.
Сейчас в репозитории в plugin/ лежат только две вещи: .claude-plugin/plugin.json и skills/example/SKILL.md. agents/, hooks/, .mcp.json пока не существуют — нужны, создавайте сами, имена каталогов ровно как в таблице выше.
Пишем skill: полный пример¶
Возьмём «генерацию release notes» — от нуля до подтверждения, что оно подключилось.
Шаг 1: создаём каталог. Имя каталога и есть имя skill, оно должно совпадать с name во frontmatter.
Шаг 2: пишем plugin/skills/release-notes/SKILL.md. Имя файла обязано быть SKILL.md, заглавными. Формат — YAML frontmatter плюс тело в Markdown, во frontmatter два поля:
| Поле | Назначение |
|---|---|
name | Идентификатор skill. Совпадает с именем каталога |
description | Модель выбирает его, глядя только на эту строку. Пишите «когда это использовать», а не «что это такое» |
Минимальный файл, который сразу работает:
---
name: release-notes
description: Использовать при подготовке release notes. Применять, когда пользователь говорит «напиши release notes», «что изменилось в этой версии», «выпуск».
---
# Release notes
## Откуда брать материал
```bash
git describe --tags --abbrev=0 # предыдущий tag
git log --oneline <предыдущий tag>..HEAD # коммиты этой версии
```
## Формат вывода
Делим на три раздела, каждый — маркированный список, каждый пункт в одну строку; пишем изменения, заметные пользователю, внутренние рефакторинги не пишем:
- **Добавлено** — что эта версия умеет из того, чего раньше не умела
- **Исправлено** — что починили, одним предложением про симптом
- **Несовместимости** — что придётся править руками при обновлении. Нечего писать — раздел не пишем вовсе
## Границы
- Номер версии не выдумывать, брать из `version` в `pyproject.toml`.
- Если неясно, заметен ли конкретный коммит пользователю, — выписать и спросить, а не решать за пользователя.
Для тела жёсткого формата нет — это просто текст, который попадёт в контекст. Ориентируйтесь на plugin/skills/example/SKILL.md: описать когда использовать, шаги, каким должен быть результат, где границы — полезнее, чем сваливать в кучу фоновые знания.
Шаг 3: убеждаемся, что он подключился. Проверяем ровно одну определённую вещь — есть каталог или нет:
cd /path/to/flower
python3 -c "from flower.core.agent import PLUGIN_DIR; print(PLUGIN_DIR, PLUGIN_DIR.is_dir())"
Только вывод /path/to/flower/plugin True означает, что тот if в build_options() сработает. Вывод False означает, что не подключилось, и в рантайме ошибки не будет — см. предупреждение ниже.
Не считайте проверкой «запустить запрос и посмотреть, вызвался ли skill example». skill вероятностен: если модель его не вызвала, это может значить и что он не установлен, и что она просто не сочла его нужным для задачи — этот сигнал две ситуации не различает. Кроме того, build_options() никогда не выставляет опцию SDK уровня сессии skills=, и попадут ли skill'ы из plugin в список, доступный координатору, на практике не проверялось. Значение True/False у PLUGIN_DIR определённо — пользуйтесь им.
Чтобы поимённо включить конкретному исполнителю несколько skill, используйте worker(..., skills=[...]) (flower/core/roles.py); имя берётся из name в SKILL.md, SDK также принимает квалифицированную форму имя_плагина:имя_skill.
В установленном flower нет plugin/ — ни при одном из трёх способов установки
PLUGIN_DIR — это три уровня вверх от flower/core/agent.py и затем plugin/. При запуске из checkout исходников это plugin/ в корне репозитория; но в wheel пакуется только каталог flower (в pyproject.toml [tool.hatch.build.targets.wheel] packages = ["flower"]), и после установки в site-packages site-packages/plugin не существует, PLUGIN_DIR.is_dir() ложно — шаг молча пропускается, без ошибки и без предупреждения.
Это не проблема контейнера, охват гораздо шире. Каждый путь в install.sh — uv tool install, pipx install, самонастройка uv с последующим uv и запасной pip install --user — ставит wheel. То есть у flower, установленного одной командой, пакет доменных возможностей всегда молча не работает. Контейнер — лишь один частный случай той же проблемы: docker/Dockerfile делает COPY только pyproject.toml, flower/, examples/, а plugin/ в образ не попадает.
Зафиксировано в issue #15. После установки первым делом прогоните команду с PLUGIN_DIR выше: вывод False означает, что в этой установке пакета доменных возможностей нет. Если он нужен, сейчас остаётся только запуск из checkout исходников.
Почему setting_sources=[] вынуждает доменные возможности идти через plugin¶
В той же функции есть ещё одна строка:
По умолчанию в SDK стоит None = читать все три источника: ~/.claude/settings.json (пользовательский), .claude/settings.json (проектный), .claude/settings.local.json (локальный). flower по умолчанию передаёт [] и отключает их все.
| Читается ли | Последствие | |
|---|---|---|
~/.claude/ (хост) | Нет | На другой машине поведение то же, результат не меняется из-за «а у меня тут настроено» |
Проектный .claude/ | Нет | То, что лежит в .claude/skills/, .claude/agents/, под flower не работает вообще |
plugin/ | Да | Путь зашит в коде, едет вместе с репозиторием |
| Учётные данные | Не по этому каналу | Обязателен собственный .env; блоки env из ~/.claude/settings.json и settings.local.json служат только последним запасным вариантом и дают лишь 9 ключей учётных данных, см. конфигурацию |
То, что .claude/ не работает, — не забытая настройка, а определение этого ограничения: стоит прочитать с хоста хотя бы один байт, и «на другой машине поведение то же» перестаёт быть правдой. Поэтому у доменных возможностей остаётся ровно один канал — plugin/, который едет с репозиторием.
Два переключателя (оба у build_options(), значения по умолчанию — переносимый вариант):
| Параметр | По умолчанию | Что будет, если поменять |
|---|---|---|
portable | True | Передать False → setting_sources становится ["project"], начинает читаться проектный .claude/ (со стороны SDK: чтобы читать CLAUDE.md, обязателен "project"). Переносимость при этом теряется |
use_plugin | True | Передать False → plugin/ не подключается вовсе, доменные возможности целиком на AgentSpec.instructions |
Попутно: instructions идёт через append (append у system_prompt), и это отдельный от plugin канал — первый присутствует в контексте каждый раунд, второй подгружается по необходимости. Короткие обязательные правила — в instructions, длинные и изредка нужные знания — в skill.
3. Сайт документации¶
Сайт, который вы читаете, собран на mkdocs-material, исходники лежат в docs/ репозитория, push в main публикует автоматически.
| Этап | Что это |
|---|---|
| Конфигурация | mkdocs.yml, docs_dir: docs |
| Многоязычность | mkdocs-static-i18n, docs_structure: folder — docs/zh/, docs/en/… язык по умолчанию zh |
| Зависимости | docs-requirements.txt (версии зафиксированы). Не extra docs из pyproject.toml — CI ставит именно первое |
| Сборка | mkdocs build --strict. Битая внутренняя ссылка или nav на несуществующую страницу сразу роняют сборку, молча выложить 404 не получится |
| Редиректы | hooks/redirects.py, после сборки пишет заглушки с meta-refresh по итоговым URL, связывая старые плоские адреса (/start/, /workflow/, /case-ht001/…) с новыми местами |
| Деплой | .github/workflows/docs.yml → actions/upload-pages-artifact@v3 + actions/deploy-pages@v4, публикация в GitHub Pages |
Правки документации локально:
pip install -r docs-requirements.txt
mkdocs serve # локальный предпросмотр
mkdocs build --strict # прогнать перед коммитом, та же команда, что и в CI
CI запускается при push в main и если изменения попали в эти пути; кроме того, можно вручную запустить workflow_dispatch на странице Actions:
Почему install.sh публикуется с Pages¶
В конце шага сборки есть строка:
Скрипт установки кладётся в артефакт сайта и таким образом висит на домене документации, а установка одной командой выглядит так:
Причина сугубо практическая: raw.githubusercontent.com из Китая недоступен, а *.github.io доступен (проверено). Сам скрипт лежит в корне репозитория, при публикации просто копируется — не нужно поддерживать две копии содержимого и не нужен отдельный CDN.
Что делает сам install.sh: выбирает установщик Python-инструментов (uv > pipx > поставить uv > pip --user), ставит flower с GitHub и подсказывает следующий шаг. Учётных данных он не трогает — первый запуск flower спросит их сам и сохранит в ~/.config/flower/.env.