Deploy e extensão¶
Levar o flower para outro lugar envolve três coisas: contêiner (cercar o Bash irrestrito e, de quebra, verificar a afirmação "sem depender do CLI"), plugin (capacidade de domínio viaja junto com o repositório, sem olhar o que está instalado na máquina host) e o site de documentação (push para main publica sozinho; o install.sh fica pendurado no domínio do Pages). As três seções são independentes; leia conforme a necessidade.
1. Contêiner¶
Por que contêiner¶
Primeiro: para cercar. O worker que faz o trabalho tem Bash irrestrito — a whitelist de Bash do flower (delegate_guard) só cobre a main thread; quem é despachado precisa poder rodar testes, então isso é intencional. No contêiner só é montado o diretório do seu projeto; o código do framework fica em /opt/flower dentro da imagem, e o resto do host não é visível.
Segundo: ele próprio é a verificação daquela restrição de portabilidade. Na imagem não há Claude Code CLI, não há Node, só Python e claude-agent-sdk — as requisições saem pelo binário nativo que vem no wheel. Se roda aqui, "sem depender do CLI" deixa de ser conversa no papel.
Verificado na prática (2026-09-06, macOS 15 / arm64 / colima + docker 28.4.0):
| O que foi verificado | Resultado |
|---|---|
| Existe CLI na imagem? | claude, node, npm, npx não existem |
| Binário embutido | \177ELF (207M) |
| Requisição real | enviada via cloud.infini-ai.com/maas e respondida, $0.1741 / 1 rodada (esse é o piso de uma única rodada com Opus 5 + janela de 1M) |
| Propriedade dos arquivos | arquivos escritos em /work dentro do contêiner aparecem no host como hechenyu:staff, mapeamento correto |
| Visibilidade do host | dentro do contêiner, ls /Users → No such file or directory |
O que está instalado na imagem¶
Imagem base python:3.13-slim, e em cima dela o apt instala apenas três pacotes. Cada um tem motivo:
| O que | Por quê |
|---|---|
python:3.13-slim | só é preciso Python ≥ 3.10. Sem Node, sem CLI do claude |
git | --isolate precisa dar uma worktree para cada subagent |
ca-certificates | passa por gateway HTTPS |
libstdc++6 | o binário embutido no SDK é um arquivo único compilado com Bun; no Linux ele precisa disso, e a imagem slim não traz |
O código do framework entra na imagem por COPY, não por bind mount — por isso o agent dentro do contêiner não alcança o código do framework no host:
| Caminho na imagem | Conteúdo | Vem de |
|---|---|---|
/opt/flower | pyproject.toml, flower/, examples/, e ali roda pip install . | COPY |
/work | diretório de trabalho (WORKDIR), onde o $PWD do host é montado em tempo de execução | docker run -v |
O entrypoint é ENTRYPOINT ["flower"] e o CMD está vazio — rodar o contêiner sem argumentos cai na entrada interativa (ele pergunta o que você quer fazer) em vez de imprimir --help. Assim não é preciso pôr aspas num pedido em linguagem natural no shell.
Por que não dá para montar o .venv do host¶
O SDK publica wheels por plataforma, e o binário embutido é específico de plataforma:
host claude_agent_sdk-0.2.152-py3-none-macosx_11_0_arm64.whl
→ _bundled/claude é Mach-O 64-bit arm64, 191M
contêiner claude_agent_sdk-0.2.152-py3-none-manylinux_2_17_aarch64.whl
Montado, não roda; por isso a imagem tem de fazer seu próprio pip install. Visto de outro ângulo, isso também é prova de portabilidade: o mesmo pyproject.toml, troca-se de plataforma e troca-se o binário nativo, sem mudar uma linha do código do framework.
Os dois scripts¶
| Script | O que faz |
|---|---|
docker/build | constrói a imagem. cd para a raiz do repositório, docker build -f docker/Dockerfile -t flower-box .; com FLOWER_MIRRORS=1 (padrão) primeiro puxa python:3.13-slim de um mirror de registry e faz retag, passando os --build-arg de pip / apt |
docker/flowerbox | roda uma vez. Checa o arquivo de credenciais → checa se $PWD pode mesmo ser montado → verifica se há TTY → docker run |
As chaves do docker/build são todas variáveis de ambiente:
| Variável | Padrão | Semântica |
|---|---|---|
FLOWER_IMAGE | flower-box | tag da imagem |
FLOWER_MIRRORS | 1 | 0 = não troca nenhum mirror, tudo direto no upstream |
FLOWER_REGISTRY | dockerproxy.net | puxa a imagem base daqui e faz retag para python:3.13-slim, para que o FROM acerte o local |
FLOWER_PIP_INDEX | https://mirrors.aliyun.com/pypi/simple/ | vai para --build-arg PIP_INDEX_URL |
FLOWER_APT_MIRROR | mirrors.ustc.edu.cn | vai para --build-arg APT_MIRROR |
As três últimas só têm efeito com FLOWER_MIRRORS=1 — o ramo FLOWER_MIRRORS=0 simplesmente não define build-arg nenhum.
O docker/flowerbox reconhece duas:
| Variável | Padrão | Semântica |
|---|---|---|
FLOWER_HOME | um nível acima da posição do próprio script (ou seja, a raiz do repositório) | onde procurar o .env. Se não achar $FLOWER_HOME/.env, sai com 1 |
FLOWER_IMAGE | flower-box | qual imagem rodar |
FLOWER_HOME é derivado da posição do próprio script, sem caminho fixo, então funciona onde quer que o repositório seja clonado.
Construir a imagem e subir o contêiner¶
docker/build # uma vez basta
cd ~/任意项目目录 # precisa estar sob $HOME, veja os limites de montagem abaixo
/path/to/flower/docker/flowerbox # sem argumentos → ele pergunta o que você quer, sem aspas
Se a rede até pypi.org / Docker Hub estiver normal, construa assim:
Os argumentos do flowerbox são exatamente os do flower — ele repassa "$@" tal e qual depois do ENTRYPOINT. --clarify-only, --asks N, --timeout 秒, --isolate, -v valem todos; a tabela completa está em Linha de comando:
cd ~/proj
/path/to/flower/docker/flowerbox --clarify-only -v
/path/to/flower/docker/flowerbox "帮我做一个 X"
O que é executado de fato é esta linha (-t só é acrescentado quando há TTY, veja abaixo):
docker run -i $TTY --rm \
--env-file "$FLOWER_HOME/.env" \
-v "$PWD:/work" \
-w /work \
"$IMAGE" "$@"
Limites de montagem e persistência¶
host $PWD ──montado──> /work ← o agent trabalha aqui, os artefatos ficam no host
na imagem /opt/flower ← código do framework, **não montado**, não alcança o host
Por isso rodar dentro de um subdiretório do próprio repositório, como flower/human-test/HT001, também é seguro: só o HT001 é montado, e o código do framework fica fora do escopo da montagem.
| Coisa | Continua lá depois de sair? | Por quê |
|---|---|---|
tudo sob o $PWD do host, incluindo runs/ e o workbench .flower/ | sim | é exatamente o diretório montado como /work |
| o que for escrito em outros caminhos dentro do contêiner | não | --rm, o contêiner é apagado ao sair |
| credenciais | não entram nas camadas da imagem | vão por --env-file; o .dockerignore exclui o .env, então nem um COPY . . levaria |
O diretório do projeto precisa estar sob $HOME, senão os artefatos somem em silêncio
O colima, por padrão, só monta $HOME dentro da VM (mount | grep virtiofs → mount0 on /Users/<você>). Rodar em lugares como /tmp faz o -v criar um diretório vazio dentro da VM; o que for escrito ali o host nunca verá, e não há erro nenhum — artefatos, brief, runs/, tudo perdido. Já aconteceu uma vez: um once terminou, $0.17 gastos, e runs/ simplesmente não existia no host.
O flowerbox agora barra esse caso: se $PWD está sob $HOME, libera direto; se não está, ele escreve um arquivo-sonda em $PWD e sobe um contêiner para testar test -f /work/<探针> de verdade (montagens extras configuradas também passam). Se não passar, sai com 1 e informa colima start --mount '<路径>:w'. A sonda precisa subir um contêiner, então antes é preciso rodar docker/build.
Como as credenciais entram no contêiner¶
Via docker run --env-file, não entram nas camadas da imagem. O flowerbox lê $FLOWER_HOME/.env, que por padrão é o .env na raiz do repositório:
Atenção: o flower setup escreve em ~/.config/flower/.env, e o flowerbox não olha esse caminho. Se você já configurou com setup e não quer duplicar, aponte FLOWER_HOME para lá:
Nomes de chave, precedência e como preencher o gateway: veja Configuração.
Sem TTY, uma pergunta trava até o --timeout
O flowerbox só acrescenta -t quando [ -t 0 ] — docker run -t num pipe / CI dá direto "the input device is not a TTY"; o -i é sempre necessário, senão a stdin nem chega.
Responder às perguntas passa pela entrada padrão. Sem TTY, o input() lança EOFError já na primeira vez → a pergunta atual é tratada como "entrada fechada" e pulada, e a thread de resposta encerra, de modo que a partir da segunda pergunta não há mais ninguém atendendo e só resta esperar o --timeout inteiro (padrão 1800 segundos). Para rodar sem supervisão, use explicitamente --timeout 0. O script imprime um aviso quando detecta ausência de TTY.
git submodule¶
O .gitmodules tem uma entrada só:
| path | url | o que é |
|---|---|---|
human-test/HT001 | https://github.com/ChenyuHeee/cppide.git | o repositório de código produzido por aquele run do HT001, guardado como registro |
Um git clone comum não puxa isso, e human-test/HT001 fica sendo um diretório vazio (o - na frente no git submodule status é exatamente esse estado). Se você precisa se importar:
| O que você quer fazer | Precisa inicializar? |
|---|---|
| rodar o flower, construir a imagem | não. O .dockerignore exclui human-test/, e o Dockerfile de qualquer forma só faz COPY de pyproject.toml / flower / examples |
| olhar localmente o código produzido no HT001 | sim: git submodule update --init human-test/HT001, ou já clonar com git clone --recurse-submodules |
Rede na China: por que aquela pilha de substituições de mirror¶
Instalar isso atrás do Great Firewall é lento não por banda, mas pela rota internacional. O docker/build padrão já troca tudo o que precisa ser trocado, e FLOWER_MIRRORS=0 desliga tudo de uma vez. Abaixo vêm os números medidos e o porquê das quatro substituições — se sua rede não tem esse problema, pule.
Tabela de velocidades medidas e as quatro substituições (2026-09-06, macOS/arm64)
| Origem | Velocidade |
|---|---|
pypi.org (índice) | 32 KB/s |
files.pythonhosted.org (arquivos de pacote) | 284 B/s |
github.com (release asset direto) | 22 KB/s |
cloud-images.ubuntu.com | 382 B/s |
deb.debian.org | 32 KB/s |
ports.ubuntu.com (dentro da VM) | 26 KB/s |
download.docker.com | inacessível (HTTP 000); 4 KB/s dentro da VM |
mirrors.tuna.tsinghua.edu.cn | inacessível |
mirrors.aliyun.com/pypi (arquivos de pacote) | 1.4 MB/s (host) / 152 KB/s (dentro da VM) |
mirrors.ustc.edu.cn/ubuntu-cloud-images | 28 MB/s |
mirrors.ustc.edu.cn/ubuntu-ports (dentro da VM) | 1.95 MB/s |
mirrors.ustc.edu.cn/debian | 435 KB/s |
ghfast.top (proxy do GitHub) | 2.5 MB/s |
gh-proxy.com (proxy do GitHub) | 1.5 MB/s |
dockerproxy.net (proxy do Docker Hub) | funciona (devolve o manifest direto) |
Ao medir, não confunda página de índice com arquivo de pacote: a página mirrors.aliyun.com/pypi/simple/ dá 7.4 MB/s, enquanto o wheel real de 95.9 MB dá só 1.4 MB/s (152 KB/s dentro da VM — a rede em user space do colima tem perda). Estime o tempo pelos números dos arquivos de pacote.
Substituição 1 — a imagem de VM do colima. O colima não usa uma cloud image comum do Ubuntu, e sim uma imagem própria com docker pré-instalado (release asset de abiosoft/colima-core), de modo que subir a VM não exige apt para instalar docker, contornando o inacessível download.docker.com. Baixe você mesmo e passe 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"
# verificação: pegue o digest pela API do GitHub. Não pule — isto vai virar uma VM em execução
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
O proxy corta o fluxo no meio (curl 56 na prática); com -C - basta retomar algumas vezes.
Substituição 2 — o apt dentro da VM. Mesmo usando a imagem com docker pré-instalado, o script de boot do lima 30-install-packages.sh ainda roda um apt-get update para instalar rsync — batendo em ports.ubuntu.com (26 KB/s) e download.docker.com (4 KB/s), o que trava por dezenas de minutos.
Solução (leia /mnt/lima-cidata/boot.sh antes de mexer: para scripts de boot que falham ele só emite WARNING + CODE=1 e segue, e no fim sempre escreve /run/lima-boot-done, então deixar aquele passo falhar é seguro):
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 segue até o fim e escreve a marca de conclusão
'
# o colima start então sai normalmente; depois instale o rsync (agora a 1.95 MB/s)
limactl shell colima -- sudo sh -c 'apt-get update -q && apt-get install -y -q rsync'
Aproveite e acrescente 127.0.0.1 lima-colima ao /etc/hosts da VM, para eliminar aquela sequência de avisos sudo: unable to resolve host.
Substituição 3 — a imagem base. O docker/build primeiro puxa python:3.13-slim de dockerproxy.net e faz retag, para que o FROM do Dockerfile acerte o local. Na prática, dockerproxy.net devolve o manifest direto (HTTP 200); docker.1ms.run / docker.m.daocloud.io devolvem 401, hub.rat.dev 302 e docker.xuanyuan.me 403.
Substituição 4 — apt e pip dentro do contêiner. --build-arg APT_MIRROR=mirrors.ustc.edu.cn (deb.debian.org 32 KB/s → USTC 435 KB/s) e --build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/. Note que PIP_INDEX_URL é também uma variável de ambiente que o próprio pip reconhece, então basta declarar o ARG para que o pip no RUN a leia — funciona mesmo sem escrever --index-url explicitamente.
O preparo único leva, naquelas condições de rede, cerca de 25 minutos, a maior parte nos 364 MB da imagem de VM e nos 95.9 MB do wheel do SDK. Depois disso, subir o flowerbox é questão de segundos.
2. plugin¶
O que é um plugin e como o SDK o carrega¶
Um pacote de capacidade de domínio que viaja com o repositório. O código do framework não contém conhecimento de domínio algum; todo o conhecimento de domínio fica no diretório plugin/ na raiz do repositório e é clonado, revisado e tagueado junto com o código.
A ligação do lado do SDK está em build_options(), em flower/core/agent.py, em duas linhas:
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)}]
Combinado com setting_sources=[] (tratado separadamente abaixo), é por isso que o flower consegue ser ao mesmo tempo "portável" e "conhecedor do seu domínio": ele não pergunta o que está instalado na máquina host, só reconhece este diretório que veio no repositório.
Layout de diretórios¶
| Caminho | O que guarda | Quando vale | Quem decide |
|---|---|---|---|
plugin/.claude-plugin/plugin.json | identidade do pacote: name, description, version, author | lido uma vez no carregamento | — |
plugin/skills/<name>/SKILL.md | conhecimento de domínio, carregado sob demanda | probabilístico — só é usado se o modelo julgar relevante | modelo |
plugin/agents/<name>.md | subagent, janela de contexto própria | delegado pelo modelo, ou indicado explicitamente no workflow | modelo / você |
plugin/hooks/hooks.json | intercepta chamadas de ferramenta | determinístico — casou, executa | código |
plugin/.mcp.json | conexão de ferramentas externas | registradas como ferramentas, iguais às embutidas | modelo |
A diferença entre probabilístico e determinístico é o critério de escolha, não uma diferença de palavras:
- skill é conhecimento parado ali. O modelo vê a
descriptione só vai lê-lo se achar relevante para a tarefa atual. O julgamento de relevância é do modelo, então a mesma frase rodada duas vezes pode usar numa e não usar na outra. - hook é código. Casou o evento, roda, independentemente de o modelo querer ou saber. O spill e a isolation do próprio flower são hooks, justamente porque não podem valer "às vezes".
Então o critério é um só: isto precisa acontecer todas as vezes? Precisa — escreva um hook. É só "bom saber" — escreva um skill. Escrever como skill algo que é obrigatório é apostar a disciplina num julgamento pontual do modelo.
Hoje o plugin/ do repositório tem só duas coisas: .claude-plugin/plugin.json e skills/example/SKILL.md. agents/, hooks/ e .mcp.json ainda não existem — se for usar, crie você mesmo, com os nomes de diretório exatamente como na tabela acima.
Escrevendo um skill: exemplo completo¶
Tomando "gerar notas de versão" como exemplo, do zero até confirmar que está valendo.
Passo 1: criar o diretório. O nome do diretório é o nome do skill, e deve bater com o name no frontmatter.
Passo 2: escrever plugin/skills/release-notes/SKILL.md. O nome do arquivo tem de ser SKILL.md, em maiúsculas. O formato é frontmatter YAML + corpo em Markdown, com dois campos no frontmatter:
| Campo | Função |
|---|---|
name | identificador do skill. Igual ao nome do diretório |
description | o modelo decide usá-lo ou não olhando só esta linha. Escreva "quando usar", não "o que é" |
Um arquivo mínimo e utilizável:
---
name: release-notes
description: Use ao preparar notas de versão. Acione quando o usuário disser "escreva as release notes", "o que mudou nesta versão", "vamos publicar".
---
# Notas de versão
## Como levantar o material
```bash
git describe --tags --abbrev=0 # tag anterior
git log --oneline <上一个 tag>..HEAD # commits desta versão
```
## Formato de saída
Divida em três blocos, cada um uma lista não ordenada, um item por linha, descrevendo mudanças perceptíveis para o usuário, sem refatorações internas:
- **Novidades** — o que esta versão passa a permitir que antes não dava
- **Correções** — o que foi corrigido, com o sintoma em uma frase
- **Incompatibilidades** — o que precisa ser mexido ao atualizar. Se não houver, omita o bloco inteiro
## Limites
- Não invente o número de versão; leia do campo `version` do `pyproject.toml`.
- Se estiver em dúvida se algum commit é perceptível para o usuário, liste e pergunte, não decida pelo usuário.
O corpo não tem formato obrigatório — é apenas um texto que será lido para dentro do contexto. Siga o estilo de plugin/skills/example/SKILL.md: deixar claro quando usar, os passos, como deve ficar a saída e onde estão os limites rende mais do que empilhar conhecimento de fundo.
Passo 3: confirmar que foi carregado. Verifique só uma coisa certa — se o diretório existe ou não:
cd /path/to/flower
python3 -c "from flower.core.agent import PLUGIN_DIR; print(PLUGIN_DIR, PLUGIN_DIR.is_dir())"
Só se imprimir /path/to/flower/plugin True é que aquele if de build_options() vai entrar. Se imprimir False, não foi carregado — e em execução não haverá erro nenhum, veja o aviso abaixo.
Não tome "rodar uma frase e ver se o skill example foi acionado" como verificação. Skill é probabilístico: se o modelo não o chamou, tanto pode ser que não esteja instalado quanto que ele simplesmente não achou necessário para a tarefa — esse sinal não distingue os dois casos. Além disso, build_options() nunca define a opção skills= de nível de sessão do SDK; se os skills do plugin realmente aparecem na lista disponível para o coordinator, isso não foi verificado na prática. O True/False daquele PLUGIN_DIR acima é determinístico; use ele.
Para habilitar skills específicos num worker, use worker(..., skills=[...]) (flower/core/roles.py); o nome é o name do SKILL.md, e o SDK também aceita a forma qualificada nome-do-plugin:nome-do-skill.
A instalação do flower não traz plugin/ — nenhuma das três formas traz
PLUGIN_DIR sobe três níveis a partir de flower/core/agent.py e entra em plugin/. Rodando de um checkout do código-fonte, isso é o plugin/ na raiz do repositório; mas o wheel empacota apenas o diretório flower (no pyproject.toml, [tool.hatch.build.targets.wheel] packages = ["flower"]), e depois de instalado em site-packages site-packages/plugin não existe, PLUGIN_DIR.is_dir() é falso — pulado em silêncio, sem erro, sem aviso.
Isso não é um problema do contêiner; o alcance é bem maior. Todos os caminhos do install.sh — uv tool install, pipx install, bootstrap do uv e depois uso do uv, e o fallback pip install --user — instalam o wheel. Ou seja, no flower instalado por uma linha de comando, o pacote de capacidade de domínio não funciona, silenciosamente. O contêiner é só mais uma instância do mesmo problema: o docker/Dockerfile só faz COPY de pyproject.toml, flower/ e examples/; plugin/ não entra na imagem.
Registrado na issue #15. Depois de instalar, rode aquele comando PLUGIN_DIR acima para checar: se sair False, esta instalação não tem pacote de capacidade de domínio. Para usá-lo, por enquanto só rodando de um checkout do código-fonte.
Por que setting_sources=[] obriga a capacidade de domínio a passar pelo plugin¶
Na mesma função há também esta linha:
O padrão do SDK é None = ler as três origens: ~/.claude/settings.json (usuário), .claude/settings.json (projeto) e .claude/settings.local.json (local). O flower passa [] por padrão, desligando todas.
| Lê? | Consequência | |
|---|---|---|
~/.claude/ (máquina host) | não | comportamento igual ao trocar de máquina, sem resultado diferente por "nesta aqui eu configurei" |
.claude/ do projeto | não | o que estiver em .claude/skills/, .claude/agents/ não vale nada sob o flower |
plugin/ | sim | caminho fixo no código, viaja com o repositório |
| credenciais | não passam por aqui | é preciso trazer o .env; os blocos env de ~/.claude/settings.json e settings.local.json só servem de último fallback e só rendem 9 chaves de credencial, veja Configuração |
O .claude/ não valer não é configuração esquecida, é a definição dessa restrição: bastaria ler um byte da máquina host para que "comportamento igual ao trocar de máquina" deixasse de valer. Sobra então um único canal para a capacidade de domínio — o plugin/ que viaja com o repositório.
Duas chaves (ambas em build_options(), com os padrões já na configuração portável):
| Parâmetro | Padrão | O que muda se você alterar |
|---|---|---|
portable | True | passar False → setting_sources vira ["project"], e o .claude/ do projeto passa a ser lido (do lado do SDK: para ler CLAUDE.md é obrigatório conter "project"). A portabilidade se perde junto |
use_plugin | True | passar False → o plugin/ não é montado de forma alguma, e a capacidade de domínio depende só de AgentSpec.instructions |
De passagem: instructions usa append (append do system_prompt), que é um canal diferente do plugin — o primeiro está no contexto em toda rodada, o segundo é carregado sob demanda. Disciplina curta e obrigatória vai em instructions; conhecimento longo e usado de vez em quando vai em skill.
3. Site de documentação¶
O site que você está lendo é feito com mkdocs-material; os arquivos-fonte estão em docs/ no repositório, e um push para main publica sozinho.
| Etapa | O que é |
|---|---|
| configuração | mkdocs.yml, docs_dir: docs |
| multilíngue | mkdocs-static-i18n, docs_structure: folder — docs/zh/, docs/en/… idioma padrão zh |
| dependências | docs-requirements.txt (versões travadas). Não o extra docs do pyproject.toml — o CI instala o primeiro |
| build | mkdocs build --strict. Links internos quebrados ou nav apontando para páginas inexistentes fazem o build falhar, em vez de publicar um 404 em silêncio |
| redirecionamentos | hooks/redirects.py, depois do build, escreve páginas-ponte com meta-refresh pelas URLs finais, ligando os antigos endereços planos (/start/, /workflow/, /case-ht001/…) às novas posições |
| deploy | .github/workflows/docs.yml → actions/upload-pages-artifact@v3 + actions/deploy-pages@v4, publicando no GitHub Pages |
Editando a documentação localmente:
pip install -r docs-requirements.txt
mkdocs serve # preview local
mkdocs build --strict # rode antes de commitar, é o mesmo comando do CI
O CI dispara em push para main e desde que as mudanças toquem estes caminhos; além disso dá para acionar workflow_dispatch manualmente na página de Actions:
Por que o install.sh é publicado pelo Pages¶
No fim da etapa de build há esta linha:
O script de instalação é enfiado no artefato do site e, com isso, fica pendurado no domínio da documentação; a instalação em uma linha fica assim:
O motivo é bem prático: raw.githubusercontent.com não passa na China, enquanto *.github.io passa (medido). O script em si fica na raiz do repositório; na publicação só é copiado mais uma vez — não é preciso manter duas cópias de conteúdo nem uma CDN extra.
O que o próprio install.sh faz: escolher um instalador de ferramentas Python (uv > pipx > instalar uv > pip --user), instalar o flower a partir do GitHub e indicar o próximo passo. Ele não toca em credenciais — na primeira execução o flower pergunta e guarda em ~/.config/flower/.env.