diff --git a/.env.example b/.env.example
index 0f938b1..f554fc7 100644
--- a/.env.example
+++ b/.env.example
@@ -1,12 +1,73 @@
-# Identidade padrão do SHOGUN
+# SHOGUN — configuração privada da instância
+#
+# Os instaladores copiam este arquivo para .env.local quando ele não existe.
+# O núcleo do bot não exige chave de API para iniciar. Preencha somente as
+# integrações que você realmente utilizar. Nunca versione .env.local.
+
+# Identidade visual padrão do produto. O dono principal é configurado por
+# `npm run setup` e fica em dados/src/config.json, não neste arquivo.
BOT_NAME=𝖘𝖍𝖔𝖌𝖚𝖓
DEFAULT_PERSONA=shogun
# Recomendado para Termux e aparelhos com pouca memória.
SHOGUN_LOW_MEMORY=false
-# Controles seguros do módulo de downloads.
+# ---------------------------------------------------------------------------
+# BunnyFy — opcional
+# ---------------------------------------------------------------------------
+# false = o bot usa caminhos locais/legados quando disponíveis.
+# true = habilita as capacidades BunnyFy configuradas abaixo.
+BUNNYFY_ENABLED=false
+BUNNYFY_BASE_URL=
+BUNNYFY_API_TOKEN=
+BUNNYFY_ALLOW_INSECURE_HTTP=false
+BUNNYFY_ACCOUNT_URL=
+BUNNYFY_CAPABILITY_TIMEOUT_MS=120000
+
+# Modos: off | primary | exclusive
+# off = não usa BunnyFy nessa capacidade
+# primary = tenta BunnyFy e usa fallback quando o contrato permitir
+# exclusive = usa BunnyFy sem fallback legado
+BUNNYFY_AI_MODE=off
+BUNNYFY_AI_TIMEOUT_MS=120000
BUNNYFY_YOUTUBE_MODE=off
BUNNYFY_YOUTUBE_TIMEOUT_MS=190000
BUNNYFY_YOUTUBE_MAX_BYTES=52428800
BUNNYFY_YOUTUBE_MAX_CONCURRENCY=4
+BUNNYFY_TRANSCRIPTION_MODE=off
+BUNNYFY_FACEBOOK_MODE=off
+BUNNYFY_PINTEREST_MODE=off
+BUNNYFY_TIKTOK_MODE=off
+BUNNYFY_KWAI_MODE=off
+BUNNYFY_IMAGES_MODE=off
+BUNNYFY_STICKERS_MODE=off
+BUNNYFY_CANVAS_MODE=off
+BUNNYFY_LOGOS_MODE=off
+BUNNYFY_GAMES_MODE=off
+BUNNYFY_IMAGE_GEN_MODE=off
+BUNNYFY_TAVERN_RENDER_MODE=off
+BUNNYFY_NEXO_RENDER_MODE=off
+
+# ---------------------------------------------------------------------------
+# IA direta NVIDIA — opcional
+# ---------------------------------------------------------------------------
+# Necessária somente quando a assistente usar NVIDIA diretamente:
+# - BUNNYFY_AI_MODE=off; ou
+# - BUNNYFY_AI_MODE=primary e você quiser fallback direto.
+# Em BUNNYFY_AI_MODE=exclusive, a credencial do provider pertence à BunnyFy.
+NVIDIA_API_KEY=
+
+# ---------------------------------------------------------------------------
+# Transcrição VEX legada — opcional
+# ---------------------------------------------------------------------------
+# Preencha apenas se sua instalação usar o fallback legado de transcrição.
+VEX_API_KEY=
+VEX_SITE=
+
+# ---------------------------------------------------------------------------
+# Upload GitHub legado — opcional
+# ---------------------------------------------------------------------------
+# Não é requisito geral do SHOGUN. Use apenas nos fluxos que ainda dependam
+# desse mecanismo e crie um token dedicado com o menor escopo possível.
+UPLOAD_GITHUB_TOKEN=
+UPLOAD_GITHUB_REPO=
diff --git a/.github/workflows/shogun-validate.yml b/.github/workflows/shogun-validate.yml
index e2e5f1c..2202b90 100644
--- a/.github/workflows/shogun-validate.yml
+++ b/.github/workflows/shogun-validate.yml
@@ -33,6 +33,8 @@ jobs:
run: npm ci --no-audit --no-fund
- name: Inspecionar plataforma
run: npm run preflight
+ - name: Validar contrato de configuração pública
+ run: node scripts/validate-instance-config-contract.mjs
- name: Validar arquitetura modular
run: |
npm run typecheck:vnext
@@ -59,7 +61,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest, windows-latest]
+ os: [ubuntu-latest, windows-latest, macos-latest]
node: ['20.19.0', '22', '24']
steps:
- name: Baixar o repositório
@@ -75,12 +77,55 @@ jobs:
shell: bash
run: |
node --check scripts/preflight-platform.mjs
+ node --check scripts/validate-instance-config-contract.mjs
node --check dados/src/.scripts/config.js
node --check dados/src/.scripts/start.js
node --check dados/src/.scripts/start-v9-fixed.js
node --check dados/src/connect.js
node --check dados/src/index.js
+ bash -n scripts/install-linux.sh
+ bash -n scripts/install-macos.sh
+ bash -n scripts/install-termux.sh
- name: Conferir configuração neutra
run: node -e "const c=require('./dados/src/config.example.json'); if(c.nomebot!=='SHOGUN'||c.numerodono!=='55DDDNUMERO') process.exit(1)"
- name: Construir arquitetura modular
run: npm run build:vnext
+
+ contrato-termux:
+ name: Contrato Termux/Android
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - name: Baixar o repositório
+ uses: actions/checkout@v4
+ - name: Preparar Node.js mínimo suportado
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20.19.0'
+ cache: npm
+ - name: Preparar FFmpeg
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y ffmpeg
+ - name: Instalar dependências como clone limpo
+ run: npm ci --no-audit --no-fund
+ - name: Validar script Android
+ run: |
+ bash -n scripts/install-termux.sh
+ grep -q 'nodejs-lts' scripts/install-termux.sh
+ grep -q 'ffmpeg' scripts/install-termux.sh
+ grep -q 'termux-tools' scripts/install-termux.sh
+ grep -q '.env.local' scripts/install-termux.sh
+ - name: Exercitar ramificação Termux do preflight
+ env:
+ TERMUX_VERSION: ci
+ PREFIX: /tmp/shogun-termux-prefix
+ run: |
+ mkdir -p "$PREFIX/bin"
+ touch "$PREFIX/bin/termux-wake-lock"
+ node scripts/preflight-platform.mjs
+ - name: Validar entradas do runtime
+ run: |
+ node --check dados/src/.scripts/start-v9-fixed.js
+ node --check dados/src/connect.js
+ node --check dados/src/index.js
diff --git a/README.md b/README.md
index 8fa63ba..a2933d6 100644
--- a/README.md
+++ b/README.md
@@ -5,193 +5,310 @@
𝖘𝖍𝖔𝖌𝖚𝖓
- Um bot para os seus grupos de WhatsApp.
- Ele modera, baixa vídeo e música, faz figurinha, joga e tem um RPG inteiro.
- Roda no seu computador ou num celular Android parado na gaveta — e é de graça.
+ Seu grupo não precisa de mais um bot. Precisa de um sistema.
+ Moderação, mídia, jogos, economia e automações em uma única experiência para WhatsApp.
-
-
-
+
+
+
+
- Assim que ele fica quando está ligado: a tela de conexão e o
- acompanhamento ao vivo de tudo que chega.
+ Instalar
+ •
+ Recursos
+ •
+ Arquitetura
+ •
+ Configuração
+ •
+ Primeiros passos
+ •
+ Problemas
---
-## 👉 Nunca instalou nada assim? Comece por aqui
-
-Escolha onde o bot vai rodar. Cada guia começa do zero, mostra **o que aparece
-na sua tela** a cada passo e o que fazer quando não aparece.
-
-
-📱 Android
-Celular reserva na tomada
-15 a 35 min
-baixar o Termux
- |
-
-🪟 Windows
-10 ou 11, no seu PC
-10 a 20 min
+ |
+
+### Um bot que realmente vive no grupo
+
+O SHOGUN foi pensado para grupos que querem mais do que meia dúzia de respostas automáticas. Ele combina administração, mídia, interação entre membros, economia persistente, RPG e recursos opcionais de IA sem transformar a instalação numa peregrinação por quinze serviços obrigatórios.
+
+- **o núcleo inicia sem chave de API**;
+- **funciona sem IA** e ganha recursos extras quando integrações são configuradas;
+- **mantém sessão e dados localmente** no aparelho onde está rodando;
+- **trata Termux como plataforma de verdade**, não como nota de rodapé;
+- **separa o núcleo do bot de serviços externos**, evitando que uma API indisponível derrube tudo.
+
|
-
-🐧 Linux
-Desktop, mini PC ou servidor
-10 a 20 min
+ |
+ 
+ Painel de conexão e primeira execução.
|
-
-
-
-
-
-
- À esquerda, a tela de conexão. À direita, o feed ao vivo — cada comando
- e mensagem que chega aparece assim no seu terminal.
+
+Mensagens e comandos chegando em tempo real. Sem painel web obrigatório, sem teatro.
---
-## Vai rodar no Android?
+## ✦ O que vem no SHOGUN
-Antes de tudo, instale o **Termux** — é o aplicativo onde o bot roda. Toque e o
-download começa:
+
+
+
+🛡️ Moderação
+Boas-vindas, anti-link, anti-flood, advertências, controles administrativos e automações para manter o grupo utilizável quando os humanos resolvem testar os limites da civilização.
+ |
+
+🎞️ Mídia
+Figurinhas, conversões, áudio, imagem e fluxos de download. Capacidades avançadas podem usar a BunnyFy quando configuradas.
+ |
+
+🎮 Jogos
+Brincadeiras, comandos sociais e experiências interativas pensadas para acontecer dentro da conversa, sem mandar todo mundo para outro aplicativo.
+ |
+
+
+
+🪙 Economia & RPG
+Progressão, inventário, recursos persistentes e sistemas de grupo que continuam existindo depois que a mensagem some do histórico.
+ |
+
+🧠 IA opcional
+O núcleo inicia sem chave de IA. Quando provedores externos são configurados, o SHOGUN libera conversação e geração de mídia sem acoplar o boot a eles.
+ |
+
+💾 Estado local
+Sessão, configuração e bancos pertencem à instalação. Atualizar o código não deveria significar sacrificar o estado vivo do bot aos deuses do `rm -rf`.
+ |
+
+
+
+---
-**[⬇️ Baixar Termux (F-Droid)](https://f-droid.org/repo/com.termux_1022.apk)**
- · [alternativa no GitHub](https://github.com/termux/termux-app/releases/download/v0.118.3/termux-app_v0.118.3%2Bgithub-debug_universal.apk)
+## ⬇ Instale onde quiser
-> Não use a versão da Play Store: está parada há anos e não funciona para isso.
+
-Depois siga o [guia do Android](docs/instalacao/termux.md), que explica o resto
-tela por tela.
+> [!IMPORTANT]
+> No Android, use preferencialmente a linha tradicional do **Termux pelo F-Droid ou GitHub Releases**. A edição do Google Play segue uma linha diferente e pode apresentar incompatibilidades que o guia do SHOGUN não assume.
-## Já usa terminal? Comece em três minutos
+### Terminal já não mete medo?
```bash
git clone https://github.com/dgreych/shogun.git
cd shogun
-bash scripts/install-linux.sh # Windows: install-windows.ps1 · Android: install-termux.sh
+bash scripts/install-linux.sh # Android: install-termux.sh | macOS: install-macos.sh
npm start
```
-Leia o QR no WhatsApp, mande `!menu` no grupo e pronto. Se a palavra "terminal"
-já assusta, comece pelo [guia da sua plataforma](#-nunca-instalou-nada-assim-comece-por-aqui) — ele explica cada
-tela, sem pressupor nada.
+No Windows:
-## Como funciona
+```powershell
+powershell -ExecutionPolicy Bypass -File .\scripts\install-windows.ps1
+npm start
+```
+
+Os instaladores preservam um `.env.local` existente ou criam um automaticamente a partir de `.env.example`, com integrações externas desligadas por padrão. A primeira execução abre o fluxo de conexão e oferece **QR Code** ou **código de pareamento**. Depois disso, a sessão é reaproveitada nas próximas inicializações.
+
+---
+
+## ◈ Como ele se organiza
```mermaid
flowchart LR
- A[Seu WhatsApp] <--> B[𝖘𝖍𝖔𝖌𝖚𝖓
no seu aparelho]
- B --> C[Grupos
moderação e jogos]
- B --> D[Downloads
YouTube, TikTok e mais]
- B -.opcional.-> E[API de IA
imagem e transcrição]
+ W[WhatsApp] <--> S[𝖘𝖍𝖔𝖌𝖚𝖓]
+ S --> G[Grupos]
+ S --> M[Mídia]
+ S --> R[Jogos · RPG · Economia]
+ S --> L[(Sessão e dados locais)]
+ S -. quando configurado .-> E[Serviços externos]
+ E -. mídia avançada .-> B[BunnyFy]
+ E -. recursos opcionais .-> A[IA / APIs]
```
-A sessão e os dados dos grupos ficam **no seu aparelho**. Nada de servidor de
-terceiro, salvo as APIs que você mesmo configurar.
+O bot continua responsável pelo fluxo do WhatsApp e pelo estado do grupo. Integrações externas entram como capacidades adicionais, não como condição para o processo existir.
-## O que ele faz
+---
-**Cuida do grupo.** Boas-vindas, anti-link, anti-flood e advertências com
-banimento automático na terceira. Moderadores com permissões próprias, separadas
-das do administrador do WhatsApp. Silenciar quem está atrapalhando, abrir e
-fechar o grupo por horário.
+## ✓ Primeira inicialização
-**Resolve mídia.** Baixa de YouTube, TikTok, Instagram, Twitter, Facebook,
-Pinterest e Kwai. Vídeo vira áudio, áudio vira texto, imagem vira figurinha e
-figurinha vira imagem. Faz figurinha animada de vídeo curto e mistura dois
-emojis num só.
+
+
+1. Instale o instalador cria o ambiente local e instala dependências. |
+2. Configure
npm run setup define o dono principal desta instância, nome e prefixo. |
+3. Verifique
npm run preflight confere sistema, configuração e integrações. |
+4. Conecte
npm start abre QR ou pareamento. |
+
+
-**Gera imagem por IA.** Texto vira imagem, remove fundo, aumenta resolução. O
-roteador escolhe o modelo pelo tipo de pedido: pedido rápido vai para o modelo
-rápido, pedido caprichado vai para o modelo de qualidade.
+
+
+
-**Conversa.** A assistente responde quando mencionada. Cada grupo escolhe a
-personalidade, e ela muda o tom das respostas e o visual dos menus junto.
+### Quem é o dono principal?
+
+```text
+SHOGUN (projeto e créditos)
+ |
+ +-- sua instalação clonada
+ |
+ +-- dono principal da instância <- seu numerodono
+ |
+ +-- sessão WhatsApp
+ |
+ +-- grupos
+ +-- admins do grupo são outra coisa
+```
-**Tem um RPG inteiro.** Trabalho, mineração, pesca, caça, forja, plantio,
-cozinha, propriedades que rendem por dia, mercado entre jogadores, habilidades
-que evoluem e ranking. Cada grupo tem a própria economia.
+O número informado no setup controla **esta cópia do bot** e os comandos reservados ao dono. Ele não muda a autoria do projeto e não significa “dono do grupo” no WhatsApp. A explicação completa, incluindo APIs e modos BunnyFy, está em **[Configuração da sua instância](docs/configuracao-da-instancia.md)**.
-**E jogos.** Velha, forca, quiz, roleta, caça-palavras e uma taverna de duelos
-por turnos.
+A configuração local fica em `dados/src/config.json`, as opções privadas ficam em `.env.local` e a sessão do WhatsApp fica em `dados/database/qr-code/`.
-## Configuração
+> [!CAUTION]
+> A pasta `dados/database/qr-code/`, `.env.local` e `dados/src/config.json` pertencem à sua instalação. **Não envie, compacte nem publique esses arquivos.** A sessão contém material de autenticação e os outros arquivos podem conter dados privados ou credenciais.
-
-
-
+---
-
- npm run preflight confere tudo que o bot precisa antes de você começar.
-
+## 🔑 APIs sem adivinhação
+
+Você **não precisa cadastrar uma pilha de chaves para chegar ao primeiro `!menu`**.
-O instalador pergunta seu nome, seu número com país e DDD, o nome do bot e o
-prefixo dos comandos. Nada disso sai do seu aparelho.
+| Situação | O que configurar |
+| --- | --- |
+| só quero instalar e usar o núcleo | setup + WhatsApp; nenhuma chave de API |
+| quero BunnyFy | URL + credencial de consumidor BunnyFy e os modos desejados |
+| quero IA NVIDIA direta | `NVIDIA_API_KEY` |
+| quero IA exclusivamente pela BunnyFy | BunnyFy ativa + `BUNNYFY_AI_MODE=exclusive`; sem chave NVIDIA no bot |
+| quero fallback VEX legado | `VEX_API_KEY` + `VEX_SITE` |
-## Manutenção
+O arquivo **[docs/configuracao-da-instancia.md](docs/configuracao-da-instancia.md)** contém a matriz completa e exemplos seguros. O `npm run preflight` mostra o que está configurado sem revelar valores.
+
+---
+
+## ⚙ Operação do dia a dia
```bash
-npm run preflight # confere Node.js, npm, Git, FFmpeg e a plataforma
-npm run setup # refaz a configuração inicial
-npm start # inicia o bot
+npm run preflight # diagnóstico do ambiente e da configuração
+npm run setup # configuração do dono/nome/prefixo
+npm start # iniciar
```
-Atualizar:
+Para atualizar uma instalação existente:
```bash
-git pull --ff-only && npm ci --no-audit --no-fund && npm start
+git pull --ff-only
+npm ci --no-audit --no-fund
+npm run preflight
+npm start
```
-## Dúvidas frequentes
+No Termux, mantenha o processo acordado com:
-**Preciso de um número separado?** Sim. Use um chip só do bot — ele conecta
-como aparelho vinculado e responde por essa conta.
+```bash
+termux-wake-lock
+```
-**Preciso deixar o computador ligado?** Sim, enquanto quiser o bot no ar. Por
-isso muita gente usa um Android antigo na tomada.
+O comando faz parte das ferramentas do Termux. **Não é necessário instalar o aplicativo Termux:API apenas para usar o wake lock.**
+
+---
-**Funciona sem chave de IA?** Funciona. Moderação, downloads, figurinhas, jogos
-e RPG não dependem de IA. Só geração de imagem e transcrição precisam.
+## ◉ Portabilidade verificada
-**Vão banir meu número?** O bot usa a conexão oficial de aparelhos vinculados.
-O que causa bloqueio é comportamento: disparo em massa e spam. Use com bom
-senso.
+A validação pública não se resume a “rodou na máquina de quem fez”. O workflow testa o projeto em combinações de Node.js sobre **Linux, Windows e macOS**, além de manter um contrato específico para os caminhos usados no **Termux**.
-**Meus dados vão para algum servidor?** Não. Sessão, bancos e configuração
-ficam no aparelho onde o bot roda.
+| Superfície | Verificação |
+| --- | --- |
+| Linux | instalação, build e validações do projeto |
+| Windows | lockfile, runtime e portabilidade |
+| macOS | Intel/Apple Silicon no caminho suportado pelo Node |
+| Termux | instalador, dependências e contrato Android |
-## Segurança
+Requisito mínimo: **Node.js 20.19+**, npm, Git e FFmpeg.
-A pasta `dados/database/qr-code/` guarda a sessão do WhatsApp. **Quem tem essa
-pasta entra na sua conta**: não compacte, não envie, não publique. Ela já está
-protegida pelo `.gitignore`.
+---
-## Requisitos
+## ? Dúvidas rápidas
+
+
+Preciso de um número separado?
+
+É fortemente recomendado. O SHOGUN funciona como aparelho vinculado à conta conectada.
+
+
+
+O que significa “dono” no setup?
+
+É o dono principal desta instalação: o número autorizado a usar comandos reservados ao dono. Não é o autor do projeto e não é automaticamente o administrador de todos os grupos. Veja Configuração da sua instância.
+
+
+
+Preciso de chave de API para iniciar?
+
+Não. O núcleo inicia sem chave de API. BunnyFy, NVIDIA, VEX e outras integrações são configuradas conforme os recursos que você quiser ativar.
+
+
+
+Preciso deixar o aparelho ligado?
+
+Sim. O processo precisa continuar rodando. No Android, use termux-wake-lock e retire o Termux da otimização agressiva de bateria.
+
+
+
+Funciona sem IA?
+
+Sim. IA e serviços externos são capacidades opcionais; o núcleo do bot não depende deles para iniciar.
+
+
+
+Onde começo se nunca usei terminal?
+
+Use o guia da sua plataforma. Ele acompanha os scripts existentes no repositório, em vez de mandar você executar uma coleção arqueológica de comandos copiados da internet.
+
-
-
-
-
-
-
+---
-Node.js 20.19 ou superior · FFmpeg · Git · um número de WhatsApp dedicado
+## Documentação
-Os instaladores cuidam disso para você. A lista está aqui para quem já tem o
-ambiente montado e quer conferir.
+**[Configuração da instância](docs/configuracao-da-instancia.md)** · **[Primeiros passos](docs/primeiros-passos.md)** · **[Segurança](docs/seguranca.md)** · **[Solução de problemas](docs/solucao-de-problemas.md)** · **[Termux](docs/instalacao/termux.md)** · **[Windows](docs/instalacao/windows.md)** · **[Linux](docs/instalacao/linux.md)** · **[macOS](docs/instalacao/macos.md)**
-## Licença
+## Licença e créditos
-Publicado sob a licença ISC. Veja [LICENSE](LICENSE) e [NOTICE](NOTICE).
+Publicado sob a licença ISC. Consulte [LICENSE](LICENSE) e [NOTICE](NOTICE).
\ No newline at end of file
diff --git a/dados/src/.scripts/validate-release-output.js b/dados/src/.scripts/validate-release-output.js
index e5e897f..568116c 100644
--- a/dados/src/.scripts/validate-release-output.js
+++ b/dados/src/.scripts/validate-release-output.js
@@ -37,7 +37,7 @@ if (fs.existsSync(runtimeIndexPath)) {
const runtimeIndex = fs.readFileSync(runtimeIndexPath, 'utf8');
assert(runtimeIndex.includes("case 'criador'"), 'comando criador presente');
assert(runtimeIndex.includes('github.com/dgreych/shogun'), 'repositório do produto presente');
- assert(runtimeIndex.includes('*Maurício Almeida*'), 'autoria do produto presente');
+ assert(runtimeIndex.includes('*Alaska dev* (Maurício)'), 'autoria do produto presente no cartão real do criador');
assert(!runtimeIndex.includes('Hiudy'), 'crédito de terceiro não aparece em saída do bot');
assert(!/sentinela/i.test(runtimeIndex), 'sem rótulo genérico na identidade');
assert(runtimeIndex.includes('Comando não reconhecido'), 'cartão de comando inválido presente');
@@ -68,6 +68,7 @@ if (fs.existsSync(packagePath)) {
assert(packageData.name === 'shogun-whatsapp', 'pacote identificado como SHOGUN');
assert(packageData.version === '2.0.0', 'versão principal definida como 2.0.0');
assert(String(packageData.description || '').startsWith('SHOGUN é seu sentinela'), 'descrição independente presente');
+ assert(packageData.author === 'Maurício Almeida', 'autoria canônica declarada no pacote');
assert(packageData.engines?.node === '>=20.19.0', 'piso real do Node.js declarado');
assert(Boolean(packageData.scripts?.preflight), 'inspeção multiplataforma disponível');
}
diff --git a/dados/src/funcs/downloads/youtube.js b/dados/src/funcs/downloads/youtube.js
index 4615cf3..2eee42f 100644
--- a/dados/src/funcs/downloads/youtube.js
+++ b/dados/src/funcs/downloads/youtube.js
@@ -7,7 +7,7 @@ import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
-import { getConfig } from '../../utils/gyomeiStore.js';
+import { getConfig } from '../../utils/shogunStore.js';
import { downloadYoutubeAudioForPlay, downloadYoutubeVideoForPlay } from '../../services/bunnyfy/youtubeGateway.js';
const DOWNLOAD_TIMEOUT = 180000;
diff --git a/dados/src/services/bunnyfy/runtimeConfig.js b/dados/src/services/bunnyfy/runtimeConfig.js
index bb33597..f5ff607 100644
--- a/dados/src/services/bunnyfy/runtimeConfig.js
+++ b/dados/src/services/bunnyfy/runtimeConfig.js
@@ -1,4 +1,4 @@
-import { getConfig } from '../../utils/gyomeiStore.js';
+import { getConfig } from '../../utils/shogunStore.js';
const BUNNYFY_CONFIG_KEYS = Object.freeze([
'BUNNYFY_ENABLED',
@@ -17,7 +17,15 @@ const BUNNYFY_CONFIG_KEYS = Object.freeze([
'BUNNYFY_STICKERS_MODE',
'BUNNYFY_CANVAS_MODE',
'BUNNYFY_LOGOS_MODE',
- 'BUNNYFY_GAMES_MODE'
+ 'BUNNYFY_GAMES_MODE',
+ 'BUNNYFY_IMAGE_GEN_MODE',
+ 'BUNNYFY_TAVERN_RENDER_MODE',
+ 'BUNNYFY_NEXO_RENDER_MODE',
+ 'BUNNYFY_TRANSCRIPTION_MODE',
+ 'BUNNYFY_FACEBOOK_MODE',
+ 'BUNNYFY_PINTEREST_MODE',
+ 'BUNNYFY_TIKTOK_MODE',
+ 'BUNNYFY_KWAI_MODE'
]);
const EXTRA_ALIASES = Object.freeze({
@@ -59,4 +67,4 @@ function resolveBunnyFyRuntimeEnv(env = process.env, config = getConfig()) {
export {
BUNNYFY_CONFIG_KEYS,
resolveBunnyFyRuntimeEnv
-};
+};
\ No newline at end of file
diff --git a/dados/src/utils/shogunMedia.js b/dados/src/utils/shogunMedia.js
new file mode 100644
index 0000000..61d42a1
--- /dev/null
+++ b/dados/src/utils/shogunMedia.js
@@ -0,0 +1,44 @@
+import { downloadContentFromMessage } from 'baileys';
+import { getQuotedMediaSource } from './shogunCore.js';
+
+async function streamToBuffer(stream) {
+ const chunks = [];
+ for await (const chunk of stream) {
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+ }
+ return Buffer.concat(chunks);
+}
+
+export async function downloadQuotedCommandMedia(message) {
+ const source = getQuotedMediaSource(message);
+ if (!source?.message) {
+ return {
+ ok: false,
+ msg: 'Responda a uma foto, GIF ou vídeo válido.'
+ };
+ }
+
+ try {
+ const stream = await downloadContentFromMessage(source.message, source.type);
+ const buffer = await streamToBuffer(stream);
+
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
+ return {
+ ok: false,
+ msg: 'A mídia citada foi reconhecida, mas veio vazia.'
+ };
+ }
+
+ return {
+ ok: true,
+ buffer,
+ type: source.type,
+ gifPlayback: source.gifPlayback === true
+ };
+ } catch (error) {
+ return {
+ ok: false,
+ msg: `Não foi possível baixar a mídia citada: ${error.message}`
+ };
+ }
+}
diff --git a/dados/src/utils/shogunOperations.js b/dados/src/utils/shogunOperations.js
new file mode 100644
index 0000000..0a2f14d
--- /dev/null
+++ b/dados/src/utils/shogunOperations.js
@@ -0,0 +1,373 @@
+import axios from 'axios';
+import fs from 'fs';
+import { downloadContentFromMessage } from 'baileys';
+import {
+ DELETED_FILE,
+ MEDIA_DIR,
+ getAutomationData,
+ getConfig,
+ normalizeCommand,
+ readJson,
+ saveAutomationData,
+ unwrapMessageContent,
+ writeJson
+} from './shogunStore.js';
+import { getActivePersona } from './shogunCore.js';
+import { transcriptionWithBunnyFy } from '../services/bunnyfy/capabilityGateway.js';
+
+const MAX_RECENT_MESSAGES = 5000;
+const commandContext = new Map();
+const deletedMessages = new Map();
+const recentMessages = new Map();
+const mediaInterceptorSockets = new WeakSet();
+const deletedTrackerSockets = new WeakSet();
+
+export function isAutoTranscriptionEnabled(chatId) {
+ return getAutomationData().autoTranscriptionGroups?.[chatId] === true;
+}
+
+export function toggleAutoTranscription(chatId) {
+ const data = getAutomationData();
+ data.autoTranscriptionGroups[chatId] = !data.autoTranscriptionGroups[chatId];
+ saveAutomationData(data);
+ return data.autoTranscriptionGroups[chatId];
+}
+
+export async function transcribeAudioUrl(audioUrl) {
+ const config = getConfig();
+ const site = String(config.site_vex || '').replace(/\/$/, '');
+ const apiKey = String(config.apikey_vex || '').trim();
+
+ if (!site || !apiKey || apiKey.startsWith('COLOQUE_')) {
+ return { ok: false, msg: 'Configure site_vex e apikey_vex em dados/src/config.json.' };
+ }
+ if (!audioUrl) return { ok: false, msg: 'Não foi possível gerar o link temporário do áudio.' };
+
+ try {
+ const url = `${site}/api/ias/transcrever?apikey=${encodeURIComponent(apiKey)}&query=${encodeURIComponent(audioUrl)}`;
+ const response = await axios.get(url, {
+ headers: { Accept: '*/*', 'User-Agent': 'Node.js' },
+ timeout: 120000
+ });
+ const data = response.data;
+ const text = data?.resultado?.data?.texto
+ || data?.resultado?.texto
+ || data?.data?.texto
+ || data?.data?.text
+ || data?.texto
+ || data?.text
+ || data?.resposta
+ || data?.result;
+
+ if (!text) {
+ return { ok: false, msg: data?.message || data?.msg || 'A API não retornou uma transcrição.' };
+ }
+ return { ok: true, texto: String(text).trim(), source: 'vex' };
+ } catch (error) {
+ return {
+ ok: false,
+ msg: error?.response?.data?.message
+ || error?.response?.data?.msg
+ || error?.response?.data?.detail
+ || error.message
+ };
+ }
+}
+
+export async function transcribeAudio(buffer, { mime = 'audio/ogg', language, uploadForFallback } = {}) {
+ try {
+ return await transcriptionWithBunnyFy(buffer, {
+ mime,
+ language,
+ legacyFallback: async () => {
+ if (typeof uploadForFallback !== 'function') {
+ return { ok: false, msg: 'Não foi possível gerar o link temporário do áudio.' };
+ }
+ const audioUrl = await uploadForFallback();
+ return transcribeAudioUrl(audioUrl);
+ }
+ });
+ } catch (error) {
+ return { ok: false, msg: error?.message || 'Não foi possível transcrever o áudio agora.' };
+ }
+}
+
+export async function saveCommandMedia(command, buffer, type, gifPlayback = false) {
+ const normalized = normalizeCommand(command);
+ if (!normalized) throw new Error('Informe o comando que receberá a mídia.');
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new Error('Mídia inválida.');
+
+ fs.mkdirSync(MEDIA_DIR, { recursive: true });
+ const normalizedType = type === 'image' ? 'image' : type === 'gif' ? 'gif' : 'video';
+ const extension = normalizedType === 'image' ? 'jpg' : normalizedType === 'gif' ? 'gif' : 'mp4';
+ const safeName = normalized.replace(/[^a-z0-9_-]/gi, '_');
+ const filePath = `${MEDIA_DIR}/${safeName}.${extension}`;
+ fs.writeFileSync(filePath, buffer);
+
+ const data = getAutomationData();
+ data.commandMedia[normalized] = {
+ type: normalizedType,
+ path: filePath,
+ gifPlayback: Boolean(gifPlayback),
+ updatedAt: new Date().toISOString()
+ };
+ saveAutomationData(data);
+ return data.commandMedia[normalized];
+}
+
+export function listCommandMedia() {
+ return Object.entries(getAutomationData().commandMedia || {})
+ .filter(([, item]) => item?.path)
+ .map(([command, item]) => ({ command, ...item }));
+}
+
+export function getCommandMedia(command) {
+ const normalized = normalizeCommand(command);
+ const item = getAutomationData().commandMedia?.[normalized];
+ return item?.path ? item : null;
+}
+
+export function resolveCommandMedia(command) {
+ const persona = getActivePersona();
+ return getCommandMedia(`${persona}_${command}`) || getCommandMedia(command);
+}
+
+export function removeCommandMedia(command) {
+ const normalized = normalizeCommand(command);
+ const data = getAutomationData();
+ const current = data.commandMedia?.[normalized];
+ if (!current) return false;
+
+ try {
+ if (current.path && fs.existsSync(current.path)) fs.unlinkSync(current.path);
+ } catch {}
+
+ delete data.commandMedia[normalized];
+ saveAutomationData(data);
+ return true;
+}
+
+function installCommandMediaInterceptor(sock) {
+ if (!sock?.sendMessage || mediaInterceptorSockets.has(sock)) return;
+ mediaInterceptorSockets.add(sock);
+
+ const originalSendMessage = sock.sendMessage.bind(sock);
+ sock.sendMessage = async (jid, content, options) => {
+ try {
+ const context = commandContext.get(jid);
+ const fresh = context && Date.now() - context.timestamp < 20000;
+ const replaceable = content && (
+ content.image
+ || content.video
+ || typeof content.text === 'string'
+ || typeof content.caption === 'string'
+ );
+
+ if (fresh && replaceable) {
+ const custom = resolveCommandMedia(context.command);
+ if (custom?.path && fs.existsSync(custom.path)) {
+ const nextContent = { ...content };
+ const caption = String(content.caption ?? content.text ?? '');
+ delete nextContent.text;
+ delete nextContent.caption;
+ delete nextContent.image;
+ delete nextContent.video;
+
+ const mediaBuffer = fs.readFileSync(custom.path);
+ if (custom.type === 'image') {
+ nextContent.image = mediaBuffer;
+ nextContent.caption = caption;
+ nextContent.mimetype = 'image/jpeg';
+ } else {
+ nextContent.video = mediaBuffer;
+ nextContent.caption = caption;
+ nextContent.gifPlayback = Boolean(custom.gifPlayback);
+ nextContent.mimetype = custom.type === 'gif' ? 'image/gif' : 'video/mp4';
+ }
+
+ commandContext.delete(jid);
+ return originalSendMessage(jid, nextContent, options);
+ }
+ }
+ } catch (error) {
+ console.error('[GYOMEI/MÍDIA] Falha ao aplicar mídia personalizada:', error.message);
+ }
+ return originalSendMessage(jid, content, options);
+ };
+}
+
+export function prepareCommandMediaContext(sock, chatId, command) {
+ const normalized = normalizeCommand(command);
+ const ignored = new Set([
+ 'setmidia', 'delmidia', 'remmidia', 'menumidia', 'listmidias',
+ 'setmidia-profilep', 'setperfilpersona', 'changeperso', 'mudarpersona',
+ 'setprompt', 'verprompt', 'resetprompt', 'prompts', 'menuprompt',
+ 'adddono', 'deldono', 'listdonos'
+ ]);
+ if (!chatId || !normalized || ignored.has(normalized)) return;
+ installCommandMediaInterceptor(sock);
+ commandContext.set(chatId, { command: normalized, timestamp: Date.now() });
+}
+
+function recentKey(remoteJid, id) {
+ return remoteJid && id ? `${remoteJid}_${id}` : '';
+}
+
+function extractDeleteKey(message) {
+ return unwrapMessageContent(message)?.protocolMessage?.key || null;
+}
+
+function cacheIncomingMessage(message) {
+ const remoteJids = [message?.key?.remoteJid, message?.key?.remoteJidAlt].filter(Boolean);
+ const id = message?.key?.id;
+ if (!id || !remoteJids.length || extractDeleteKey(message)) return;
+
+ for (const remoteJid of remoteJids) recentMessages.set(recentKey(remoteJid, id), message);
+ while (recentMessages.size > MAX_RECENT_MESSAGES) {
+ const oldest = recentMessages.keys().next().value;
+ if (!oldest) break;
+ recentMessages.delete(oldest);
+ }
+}
+
+function rememberDeleted(chatId, message) {
+ if (!chatId || !message?.key?.id) return;
+ const list = deletedMessages.get(chatId) || [];
+ if (list.some(item => item?.key?.id === message.key.id)) return;
+ list.unshift(message);
+ deletedMessages.set(chatId, list.slice(0, 5));
+
+ const serializable = {};
+ for (const [jid, items] of deletedMessages.entries()) serializable[jid] = items.slice(0, 5);
+ writeJson(DELETED_FILE, serializable);
+}
+
+function findCachedMessage(cacheGetter, key) {
+ if (!key?.id) return null;
+ const possibleChats = [key.remoteJid, key.remoteJidAlt].filter(Boolean);
+ for (const chatId of possibleChats) {
+ const internal = recentMessages.get(recentKey(chatId, key.id));
+ if (internal) return internal;
+ }
+
+ const cache = typeof cacheGetter === 'function' ? cacheGetter() : cacheGetter;
+ if (!cache) return null;
+ for (const chatId of possibleChats) {
+ const external = cache.get?.(recentKey(chatId, key.id));
+ if (external) return external;
+ }
+ return null;
+}
+
+export function installDeletedMessageTracker(sock, cacheGetter) {
+ if (!sock?.ev || deletedTrackerSockets.has(sock)) return;
+ deletedTrackerSockets.add(sock);
+
+ const persisted = readJson(DELETED_FILE, {});
+ for (const [jid, items] of Object.entries(persisted)) {
+ if (Array.isArray(items)) deletedMessages.set(jid, items.slice(0, 5));
+ }
+
+ const captureKey = key => {
+ const original = findCachedMessage(cacheGetter, key);
+ if (original) {
+ const chatId = key.remoteJid || original.key?.remoteJid;
+ if (chatId) rememberDeleted(chatId, original);
+ }
+ };
+
+ sock.ev.on('messages.delete', event => {
+ const keys = Array.isArray(event?.keys) ? event.keys : Array.isArray(event) ? event : [];
+ keys.forEach(captureKey);
+ });
+
+ sock.ev.on('messages.update', updates => {
+ if (!Array.isArray(updates)) return;
+ for (const item of updates) {
+ const key = extractDeleteKey(item?.update?.message || item?.message);
+ if (key) captureKey(key);
+ }
+ });
+
+ sock.ev.on('messages.upsert', event => {
+ for (const message of event?.messages || []) {
+ const deleteKey = extractDeleteKey(message);
+ if (deleteKey) captureKey(deleteKey);
+ else cacheIncomingMessage(message);
+ }
+ });
+}
+
+async function streamToBuffer(stream) {
+ const chunks = [];
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+ return Buffer.concat(chunks);
+}
+
+export async function returnDeletedMessage(sock, chatId, position, quotedMessage) {
+ const index = Number(position) - 1;
+ if (!Number.isInteger(index) || index < 0 || index > 4) {
+ return { ok: false, msg: 'Use return1 a return5 ou return 1 a return 5.' };
+ }
+
+ const original = (deletedMessages.get(chatId) || [])[index];
+ if (!original?.message) {
+ return { ok: false, msg: `Não há mensagem apagada na posição ${position}.` };
+ }
+
+ const content = unwrapMessageContent(original.message);
+ const sender = original.key?.participant || original.key?.remoteJid;
+ const prefix = sender
+ ? `🗑️ *Mensagem apagada #${position}*\n👤 @${String(sender).split('@')[0]}\n\n`
+ : `🗑️ *Mensagem apagada #${position}*\n\n`;
+ const mentions = sender ? [sender] : [];
+ const options = quotedMessage ? { quoted: quotedMessage } : undefined;
+
+ try {
+ const text = content?.conversation || content?.extendedTextMessage?.text;
+ if (text) {
+ await sock.sendMessage(chatId, { text: `${prefix}${text}`, mentions }, options);
+ return { ok: true };
+ }
+
+ const candidates = [
+ ['imageMessage', 'image'],
+ ['videoMessage', 'video'],
+ ['audioMessage', 'audio'],
+ ['stickerMessage', 'sticker'],
+ ['documentMessage', 'document']
+ ];
+
+ for (const [field, type] of candidates) {
+ const media = content?.[field];
+ if (!media) continue;
+ const stream = await downloadContentFromMessage(media, type);
+ const buffer = await streamToBuffer(stream);
+ const caption = media.caption ? `${prefix}${media.caption}` : prefix.trim();
+ const payload = { mentions };
+
+ if (type === 'image') Object.assign(payload, { image: buffer, caption });
+ if (type === 'video') Object.assign(payload, { video: buffer, caption, gifPlayback: media.gifPlayback === true });
+ if (type === 'audio') Object.assign(payload, {
+ audio: buffer,
+ mimetype: media.mimetype || 'audio/ogg; codecs=opus',
+ ptt: media.ptt === true
+ });
+ if (type === 'sticker') Object.assign(payload, { sticker: buffer });
+ if (type === 'document') Object.assign(payload, {
+ document: buffer,
+ fileName: media.fileName || 'arquivo',
+ mimetype: media.mimetype || 'application/octet-stream',
+ caption
+ });
+
+ await sock.sendMessage(chatId, payload, options);
+ return { ok: true };
+ }
+
+ await sock.sendMessage(chatId, { text: `${prefix}[Tipo de mensagem ainda não suportado]`, mentions }, options);
+ return { ok: true };
+ } catch (error) {
+ return { ok: false, msg: `Não foi possível recuperar a mídia: ${error.message}` };
+ }
+}
diff --git a/dados/src/utils/shogunRuntime.js b/dados/src/utils/shogunRuntime.js
new file mode 100644
index 0000000..d79971c
--- /dev/null
+++ b/dados/src/utils/shogunRuntime.js
@@ -0,0 +1,4 @@
+export { getConfig } from './shogunStore.js';
+export * from './shogunCore.js';
+export * from './shogunOperations.js';
+export * from './shogunMedia.js';
diff --git a/dados/src/utils/shogunStore.js b/dados/src/utils/shogunStore.js
new file mode 100644
index 0000000..d8e7543
--- /dev/null
+++ b/dados/src/utils/shogunStore.js
@@ -0,0 +1,149 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+import { DEFAULT_NVIDIA_MODEL, isKnownNvidiaModel } from './nvidiaApi.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+export const SRC_DIR = path.resolve(__dirname, '..');
+export const DATABASE_DIR = path.resolve(SRC_DIR, '..', 'database');
+export const OWNER_DB_DIR = path.join(DATABASE_DIR, 'dono');
+export const AUTOMATIONS_FILE = path.join(OWNER_DB_DIR, 'automacoes-v9.json');
+export const MEDIA_DIR = path.join(OWNER_DB_DIR, 'command-media');
+export const DELETED_FILE = path.join(OWNER_DB_DIR, 'deleted-messages-v9.json');
+export const CONFIG_FILE = path.join(SRC_DIR, 'config.json');
+
+export const DEFAULT_DATA = {
+ autoTranscriptionGroups: {},
+ commandMedia: {},
+ additionalOwners: [],
+ assistantPrompts: {},
+ activePersona: 'gyomei'
+};
+
+export function ensureDirectories() {
+ fs.mkdirSync(OWNER_DB_DIR, { recursive: true });
+ fs.mkdirSync(MEDIA_DIR, { recursive: true });
+}
+
+function reviveJsonValue(_key, value) {
+ if (
+ value
+ && value.type === 'Buffer'
+ && Array.isArray(value.data)
+ && value.data.every(item => Number.isInteger(item) && item >= 0 && item <= 255)
+ ) {
+ return Buffer.from(value.data);
+ }
+ return value;
+}
+
+export function readJson(file, fallback) {
+ try {
+ return JSON.parse(fs.readFileSync(file, 'utf8'), reviveJsonValue);
+ } catch {
+ return fallback;
+ }
+}
+
+export function writeJson(file, value) {
+ ensureDirectories();
+ const temporary = `${file}.tmp`;
+ fs.writeFileSync(
+ temporary,
+ JSON.stringify(value, (_, item) => typeof item === 'bigint' ? item.toString() : item, 2)
+ );
+ fs.renameSync(temporary, file);
+}
+
+export function getAutomationData() {
+ const stored = readJson(AUTOMATIONS_FILE, {});
+ return {
+ ...DEFAULT_DATA,
+ ...stored,
+ autoTranscriptionGroups: stored.autoTranscriptionGroups || {},
+ commandMedia: stored.commandMedia || {},
+ additionalOwners: Array.isArray(stored.additionalOwners) ? stored.additionalOwners : [],
+ assistantPrompts: stored.assistantPrompts || {}
+ };
+}
+
+export function saveAutomationData(data) {
+ writeJson(AUTOMATIONS_FILE, data);
+}
+
+export function getConfig() {
+ const stored = readJson(CONFIG_FILE, {});
+ return {
+ ...stored,
+ nvidia_model: isKnownNvidiaModel(stored.nvidia_model) ? stored.nvidia_model : DEFAULT_NVIDIA_MODEL,
+ apikey_vex: process.env.VEX_API_KEY || stored.apikey_vex || '',
+ site_vex: process.env.VEX_SITE || stored.site_vex || '',
+ upload_github_token: process.env.UPLOAD_GITHUB_TOKEN || stored.upload_github_token || '',
+ upload_github_repo: process.env.UPLOAD_GITHUB_REPO || stored.upload_github_repo || 'uploadsnew/uploads'
+ };
+}
+
+export function normalizeCommand(command) {
+ return String(command || '')
+ .trim()
+ .replace(/^[!./#]+/, '')
+ .toLowerCase();
+}
+
+export function normalizeIdentity(value) {
+ const raw = String(value || '').trim().replace(/^@/, '');
+ if (!raw) return '';
+ if (raw.includes('@')) return raw.split(':')[0].toLowerCase();
+ const digits = raw.replace(/\D/g, '');
+ return digits || raw.toLowerCase();
+}
+
+export function identityAliases(value) {
+ const normalized = normalizeIdentity(value);
+ if (!normalized) return new Set();
+ const aliases = new Set([normalized]);
+ const base = normalized.split('@')[0].split(':')[0].replace(/\D/g, '');
+ if (base) aliases.add(base);
+ if (/^\d+$/.test(normalized)) {
+ aliases.add(`${normalized}@s.whatsapp.net`);
+ aliases.add(`${normalized}@lid`);
+ }
+ return aliases;
+}
+
+export function identitiesMatch(left, right) {
+ const leftAliases = identityAliases(left);
+ const rightAliases = identityAliases(right);
+ return [...leftAliases].some(alias => rightAliases.has(alias));
+}
+
+export function unwrapMessageContent(message) {
+ let current = message?.message || message || null;
+ const wrappers = [
+ 'ephemeralMessage',
+ 'viewOnceMessage',
+ 'viewOnceMessageV2',
+ 'viewOnceMessageV2Extension',
+ 'documentWithCaptionMessage',
+ 'editedMessage'
+ ];
+
+ for (let depth = 0; depth < 8 && current; depth++) {
+ const wrapper = wrappers.find(key => current?.[key]?.message);
+ if (!wrapper) break;
+ current = current[wrapper].message;
+ }
+ return current;
+}
+
+export function contextInfoFromContent(content) {
+ return content?.extendedTextMessage?.contextInfo
+ || content?.imageMessage?.contextInfo
+ || content?.videoMessage?.contextInfo
+ || content?.audioMessage?.contextInfo
+ || content?.documentMessage?.contextInfo
+ || content?.stickerMessage?.contextInfo
+ || null;
+}
diff --git a/docs/configuracao-da-instancia.md b/docs/configuracao-da-instancia.md
new file mode 100644
index 0000000..8a654a2
--- /dev/null
+++ b/docs/configuracao-da-instancia.md
@@ -0,0 +1,280 @@
+# Configuração da sua instância do SHOGUN
+
+Este guia responde três perguntas antes que você precise abrir código:
+
+1. **quem é o dono desta instalação?**
+2. **o que é obrigatório para o bot iniciar?**
+3. **quais chaves de API só são necessárias para recursos opcionais?**
+
+O objetivo é que uma instalação nova seja previsível. Nada de descobrir uma
+credencial escondida depois de quinze minutos encarando um stack trace como se
+ele fosse um oráculo.
+
+---
+
+## 1. Quem é o “dono” do bot?
+
+Quando você clona o SHOGUN e executa `npm run setup`, o número informado em
+`numerodono` define o **dono principal daquela instância**.
+
+```text
+Projeto SHOGUN
+│
+├── autoria e créditos do projeto
+│ └── continuam os mesmos em qualquer clone
+│
+└── sua instalação
+ │
+ ├── dono principal da instância
+ │ └── seu número configurado em numerodono
+ │
+ ├── sessão WhatsApp desta instalação
+ │
+ └── grupos onde o bot participa
+ └── administradores do grupo ≠ dono principal da instância
+```
+
+### Em termos simples
+
+- **Dono principal da instância:** pessoa que controla esta cópia do bot e pode
+ usar comandos reservados ao dono.
+- **Administrador de grupo:** cargo concedido pelo próprio WhatsApp dentro de um
+ grupo. Não transforma alguém em dono da instância.
+- **Autor/criador do projeto:** crédito de quem criou ou desenvolveu o software.
+ Clonar o projeto não altera autoria.
+
+O número do dono deve incluir país + DDD + número e conter somente dígitos.
+Exemplo de formato brasileiro: `55DDDNUMERO`.
+
+A configuração fica em `dados/src/config.json`. Esse arquivo é local e não deve
+ser enviado ao GitHub.
+
+---
+
+## 2. O que é obrigatório para o SHOGUN funcionar?
+
+### Núcleo do bot
+
+Para iniciar o núcleo e conectar ao WhatsApp, você precisa de:
+
+- Node.js compatível;
+- npm;
+- Git;
+- FFmpeg;
+- `dados/src/config.json`, criado pelo setup;
+- uma sessão WhatsApp criada por QR Code ou código de pareamento.
+
+**Nenhuma chave de API é obrigatória apenas para o bot iniciar.** Recursos que
+dependem de serviços externos ficam disponíveis conforme você os configura.
+
+Execute:
+
+```bash
+npm run preflight
+```
+
+O diagnóstico informa o que está pronto sem imprimir seus segredos.
+
+---
+
+## 3. `.env.local`: onde ficam as opções privadas
+
+O SHOGUN carrega configurações privadas de:
+
+```text
+.env.local
+```
+
+O repositório fornece um modelo seguro:
+
+```text
+.env.example
+```
+
+Os instaladores oficiais criam `.env.local` a partir do exemplo quando ele
+não existe. Se estiver fazendo o processo manualmente:
+
+### Linux, macOS ou Termux
+
+```bash
+cp -n .env.example .env.local
+chmod 600 .env.local
+```
+
+### Windows PowerShell
+
+```powershell
+if (-not (Test-Path .env.local)) { Copy-Item .env.example .env.local }
+```
+
+Nunca publique `.env.local`.
+
+---
+
+## 4. Matriz de integrações e credenciais
+
+| Recurso | Precisa de chave? | Configuração | Quando usar |
+| --- | --- | --- | --- |
+| Núcleo + WhatsApp | **não** | setup + sessão | sempre |
+| BunnyFy | sim, se ativada | `BUNNYFY_ENABLED`, `BUNNYFY_BASE_URL`, `BUNNYFY_API_TOKEN` | capacidades servidas pela BunnyFy |
+| Assistente via BunnyFy | chave BunnyFy | `BUNNYFY_AI_MODE=exclusive` | IA totalmente pela BunnyFy |
+| Assistente BunnyFy + fallback NVIDIA | BunnyFy + NVIDIA | `BUNNYFY_AI_MODE=primary`, `NVIDIA_API_KEY` | BunnyFy primeiro, NVIDIA direta se houver falha transitória |
+| Assistente NVIDIA direta | NVIDIA | `BUNNYFY_AI_MODE=off`, `NVIDIA_API_KEY` | IA sem BunnyFy |
+| Transcrição via BunnyFy | chave BunnyFy | BunnyFy ativa + modo correspondente | caminho recomendado quando disponível |
+| Transcrição VEX legado | VEX | `VEX_API_KEY`, `VEX_SITE` | somente quando o fallback legado for usado |
+| Upload GitHub legado | GitHub | `UPLOAD_GITHUB_TOKEN`, `UPLOAD_GITHUB_REPO` | somente nos fluxos legados que ainda dependam dele |
+
+### Regra importante sobre BunnyFy
+
+A BunnyFy é uma plataforma separada. Se uma instância do SHOGUN usa a BunnyFy,
+o bot recebe **somente uma credencial de consumidor da BunnyFy**.
+
+Chaves internas de provedores usados pela BunnyFy, como NVIDIA ou outros
+serviços da infraestrutura da API, pertencem ao operador da BunnyFy e **não
+devem ser copiadas para o bot**.
+
+---
+
+## 5. Modos BunnyFy
+
+A chave mestra é:
+
+```env
+BUNNYFY_ENABLED=false
+```
+
+Com `false`, as capacidades BunnyFy ficam desligadas e os caminhos legados ou
+locais continuam sendo usados quando existirem.
+
+Ao ativar:
+
+```env
+BUNNYFY_ENABLED=true
+BUNNYFY_BASE_URL=https://SUA-BUNNYFY.example
+BUNNYFY_API_TOKEN=SUA_CREDENCIAL_DE_CONSUMIDOR
+```
+
+Cada família usa um modo:
+
+```text
+off -> não usa BunnyFy nessa capacidade
+primary -> tenta BunnyFy; em falha transitória usa fallback quando houver
+exclusive -> usa BunnyFy; não cai para o provider legado
+```
+
+As famílias atuais incluem IA, YouTube, imagens, stickers, canvas, logos,
+geração visual, Tavern e NEXO. O `.env.example` é a referência dos nomes
+exatos disponíveis nesta versão.
+
+> Não ative `exclusive` em uma capacidade antes de confirmar que a BunnyFy
+> configurada oferece aquele contrato.
+
+---
+
+## 6. Configurações da IA
+
+### Opção A — IA totalmente pela BunnyFy
+
+```env
+BUNNYFY_ENABLED=true
+BUNNYFY_AI_MODE=exclusive
+BUNNYFY_BASE_URL=https://SUA-BUNNYFY.example
+BUNNYFY_API_TOKEN=SUA_CREDENCIAL
+NVIDIA_API_KEY=
+```
+
+O bot não precisa conhecer a chave do provedor de IA da BunnyFy.
+
+### Opção B — NVIDIA direta
+
+```env
+BUNNYFY_ENABLED=false
+NVIDIA_API_KEY=SUA_CHAVE_NVIDIA
+```
+
+O modelo pode ser escolhido pelos mecanismos do próprio bot.
+
+### Opção C — BunnyFy com fallback NVIDIA
+
+```env
+BUNNYFY_ENABLED=true
+BUNNYFY_AI_MODE=primary
+BUNNYFY_BASE_URL=https://SUA-BUNNYFY.example
+BUNNYFY_API_TOKEN=SUA_CREDENCIAL
+NVIDIA_API_KEY=SUA_CHAVE_NVIDIA
+```
+
+Aqui a chave NVIDIA é necessária para o fallback direto realmente funcionar.
+
+---
+
+## 7. VEX e integrações legadas
+
+O SHOGUN ainda preserva alguns fallbacks para não remover recursos antes de uma
+substituição comprovada.
+
+Para o fallback VEX de transcrição:
+
+```env
+VEX_API_KEY=
+VEX_SITE=
+```
+
+Deixe vazio se você não usa esse caminho.
+
+Da mesma forma, `UPLOAD_GITHUB_TOKEN` não é requisito geral do bot. Só configure
+credenciais legadas quando você souber qual recurso as consome.
+
+---
+
+## 8. Como verificar sem vazar segredo
+
+Use:
+
+```bash
+npm run preflight
+```
+
+E, quando estiver configurando NVIDIA direta:
+
+```bash
+npm run test:nvidia:live
+```
+
+O preflight deve mostrar **presença/ausência e modo**, nunca o valor de tokens.
+
+Nunca cole em issue, print, vídeo ou pedido de suporte:
+
+- `.env.local`;
+- `dados/src/config.json`;
+- `dados/database/qr-code/`;
+- tokens e chaves;
+- cookies;
+- URLs assinadas.
+
+---
+
+## 9. Sequência recomendada para uma instalação nova
+
+```text
+1. instalar Git + Node + FFmpeg
+ ↓
+2. clonar o repositório
+ ↓
+3. executar o instalador da sua plataforma
+ ↓
+4. informar o dono principal da instância
+ ↓
+5. rodar npm run preflight
+ ↓
+6. iniciar com npm start
+ ↓
+7. conectar o WhatsApp
+ ↓
+8. testar !menu
+ ↓
+9. só então ativar APIs opcionais que você realmente quiser
+```
+
+Isso mantém o primeiro boot simples e deixa integrações externas como extensão,
+não como pedágio para chegar ao menu.
diff --git a/docs/instalacao/linux.md b/docs/instalacao/linux.md
index 44557af..e991672 100644
--- a/docs/instalacao/linux.md
+++ b/docs/instalacao/linux.md
@@ -1,19 +1,31 @@
-# SHOGUN no Linux: do terminal ao primeiro menu
+🐧 SHOGUN no Linux
+Instalação em desktop ou servidor, com cada etapa verificável.
-Este guia atende computadores e servidores Linux. Se é sua primeira vez,
-Ubuntu ou Debian oferecem o caminho mais simples. Reserve cerca de 20 minutos.
+
+
+
+
+
-## Etapa 1 — abrir o Terminal
+## Rota completa
-No Ubuntu, pressione `Ctrl+Alt+T`. Em outras distribuições, procure o aplicativo
-**Terminal** no menu. Copie uma caixa por vez, cole com `Ctrl+Shift+V` e
-pressione Enter.
+| Etapa | Ação | Sinal de sucesso |
+| --- | --- | --- |
+| **1** | instalar Git e FFmpeg | versões aparecem no terminal |
+| **2** | instalar Node.js | `node --version` ≥ 20.19 |
+| **3** | clonar o SHOGUN | pasta `~/shogun` criada |
+| **4** | executar instalador | setup concluído |
+| **5** | rodar preflight | requisitos obrigatórios aprovados |
+| **6** | conectar WhatsApp | feed do bot ativo |
+| **7** | testar | `!menu` responde |
-## Etapa 2 — instalar Git e FFmpeg
+---
-Use somente o bloco da sua distribuição.
+## 1 · Prepare o sistema
-### Ubuntu, Debian, Linux Mint e derivados
+Use **somente** o bloco correspondente à sua distribuição.
+
+### Ubuntu · Debian · Linux Mint
```bash
sudo apt update
@@ -32,161 +44,230 @@ sudo dnf install -y git ffmpeg curl
sudo pacman -Syu --needed git ffmpeg curl
```
-Quando `sudo` pedir a senha, digite mesmo que nenhum caractere apareça e
-pressione Enter. Isso é uma proteção normal do Linux.
+> [!NOTE]
+> Quando `sudo` pede a senha, nenhum caractere aparece enquanto você digita. Isso é normal. O terminal não travou, ele só decidiu que feedback visual era luxo.
-## Etapa 3 — instalar Node.js LTS
+---
-O SHOGUN exige Node.js 20.19 ou mais recente e recomenda a linha LTS 22 ou 24.
-Se sua distribuição já oferece uma dessas versões, instale `nodejs` e `npm`
-pelo gerenciador de pacotes dela. Caso contrário, siga o método mostrado na
-página oficial [Baixar Node.js](https://nodejs.org/en/download).
+## 2 · Instale Node.js
-Não use Node.js 18. Depois de instalar, feche e abra o Terminal.
+O SHOGUN exige **Node.js 20.19.0 ou superior**. Para uma instalação nova, prefira uma linha LTS atual suportada pelo projeto.
-## Etapa 4 — conferir o terreno
+Se sua distribuição já oferece uma versão adequada, instale `nodejs` e `npm` pelo gerenciador de pacotes. Caso contrário, use o método indicado na página oficial do Node.js.
-Rode um por vez:
+Depois confira:
```bash
node --version
-```
-
-```bash
npm --version
-```
-
-```bash
git --version
-```
-
-```bash
ffmpeg -version
```
-Cada comando deve mostrar uma versão. Para Node.js, espere algo como `v22...`
-ou `v24...`.
+Se Node mostrar `v18`, ainda não terminou esta etapa.
-## Etapa 5 — baixar o SHOGUN
+---
-Volte à sua pasta pessoal:
+## 3 · Baixe o projeto
```bash
-cd
+cd ~
+git clone https://github.com/dgreych/shogun.git
+cd shogun
```
-Baixe o projeto:
+Confira:
```bash
-git clone https://github.com/dgreych/shogun.git
+pwd
```
-Entre na pasta:
+O caminho deve terminar em `/shogun`.
-```bash
-cd shogun
-```
+---
-## Etapa 6 — preparar e configurar
+## 4 · Execute o instalador
```bash
bash scripts/install-linux.sh
```
-O instalador baixa os componentes e faz quatro perguntas:
+O instalador cria `.env.local` a partir de `.env.example` quando necessário,
+executa o preflight, instala as dependências travadas em `package-lock.json` e
+abre a configuração inicial. Um `.env.local` que já exista é preservado.
+
+
+| Seu nome | identificação do dono principal desta instância |
+| Número | país + DDD + número do dono principal, somente dígitos |
+| Nome do bot | nome desta instalação |
+| Prefixo | por exemplo ! |
+
+
+### O dono desta instalação
-1. como o SHOGUN deve chamar você;
-2. seu número com país e DDD, somente dígitos — exemplo fictício
- `5511999999999`;
-3. nome do bot — pressione Enter para manter `SHOGUN`;
-4. prefixo — pressione Enter para manter `!`.
+```text
+projeto SHOGUN
+ └── sua instalação Linux
+ ├── dono principal -> numerodono
+ ├── sessão WhatsApp
+ └── grupos -> administradores próprios
+```
-Quando aparecer **SHOGUN pronto**, a configuração local está protegida e a
-instalação terminou. Para refazer apenas as perguntas:
+`numerodono` identifica quem controla **esta instância** e pode usar comandos de
+dono. Não altera a autoria do projeto e não transforma esse número no
+administrador de todos os grupos.
+
+Para refazer apenas o setup depois:
```bash
npm run setup
```
-## Etapa 7 — inspeção e conexão
+A matriz de BunnyFy, NVIDIA, VEX e demais opções está em
+**[Configuração da sua instância](../configuracao-da-instancia.md)**.
+
+---
+
+## 5 · Verifique antes de iniciar
```bash
npm run preflight
```
-
+
-Imagem gerada da execução real do comando. O aviso amarelo sobre
-configuração é esperado antes da etapa seguinte.
+O comando verifica Node.js, npm, Git, FFmpeg, configuração, dono principal,
+dependências e o estado das integrações sem revelar tokens.
+
+> [!NOTE]
+> O núcleo não precisa de chave de API para chegar ao primeiro `!menu`. Avisos
+> sobre integrações opcionais só significam que aquele recurso específico ainda
+> não foi configurado.
-Com os itens obrigatórios aprovados, inicie:
+---
+
+## 6 · Inicie e conecte
```bash
npm start
```
-Escolha `1` para QR Code. No telefone, abra WhatsApp → menu de três pontos →
-**Aparelhos conectados/Dispositivos conectados → Conectar um aparelho** e leia
-o QR do terminal.
+Escolha **QR Code** ou **código de pareamento** no painel.
+
+
+
+
+
+No telefone, para QR, abra **WhatsApp → Aparelhos conectados → Conectar um aparelho**. Se QR não for conveniente, reinicie o fluxo e escolha pareamento por código.
+
+A sessão criada é reutilizada nas próximas inicializações enquanto continuar válida.
-Se o WhatsApp estiver no mesmo equipamento ou o QR não couber, reinicie com
-`Ctrl+C`, escolha `2` e use o código de pareamento.
+---
-## Etapa 8 — confirmar a primeira missão
+## 7 · Teste de aceite
-Numa conversa de teste, envie:
+Envie em um grupo de teste:
```text
!menu
```
-Se o menu chegou, o posto está pronto. O Terminal precisa permanecer aberto
-enquanto o SHOGUN estiver em serviço.
+Se escolheu outro prefixo, substitua `!`.
-## Sua rotina
+**Recebeu resposta?** O núcleo instalou, conectou e está despachando comandos.
-Para parar, pressione `Ctrl+C`.
+---
-Para voltar outro dia:
+## APIs opcionais
-```bash
-cd ~/shogun
-```
+Depois do primeiro `!menu`, edite `.env.local` apenas se quiser capacidades
+externas. Resumo:
+
+- núcleo + WhatsApp: sem chave de API;
+- BunnyFy: `BUNNYFY_ENABLED=true`, URL e credencial de consumidor;
+- NVIDIA direta: `NVIDIA_API_KEY` para IA direta ou fallback;
+- VEX legado: `VEX_API_KEY` + `VEX_SITE` somente quando esse fallback for usado.
+
+Veja **[Configuração da sua instância](../configuracao-da-instancia.md)** antes
+de ativar modos `primary` ou `exclusive`.
+
+---
+
+## Uso diário
+
+### Parar
+
+`Ctrl+C`
+
+### Iniciar de novo
```bash
+cd ~/shogun
npm start
```
-Para atualizar, pare o processo e rode:
+### Atualizar
```bash
+cd ~/shogun
git pull --ff-only
-```
-
-```bash
npm ci --no-audit --no-fund
-```
-
-```bash
+npm run preflight
npm start
```
-## Servidor ligado continuamente
+> [!TIP]
+> Em servidor que ficará ligado continuamente, faça primeiro a instalação manual e confirme `!menu`. Só depois configure supervisão de processo. Isso separa problema de instalação de problema de serviço, uma pequena gentileza para o seu futuro eu.
+
+Veja **[Implantação em servidor](../../DEPLOY.md)** para uma instalação pública supervisionada.
+
+---
+
+## Diagnóstico
+
+
+sudo: command not found
+
+Sua distribuição usa outro método de administração ou você está num ambiente restrito. Instale Git, Node.js e FFmpeg pelo mecanismo adequado ao sistema antes de continuar.
+
+
+
+Node ainda mostra v18
+
+Instale uma versão compatível e abra um novo terminal. Depois confirme com node --version e npm run preflight.
+
+
+
+Permissão negada no instalador
+
+Use bash scripts/install-linux.sh. Não é necessário marcar o arquivo executável para esse fluxo.
+
+
+
+A pasta shogun já existe
+
+Entre nela com cd ~/shogun; não clone outra cópia por cima.
+
+
+---
+
+## 🔐 Proteja o estado local
+
+A sessão e a configuração devem sobreviver às atualizações:
+
+```text
+dados/database/
+dados/src/config.json
+.env.local
+```
-Primeiro conclua a instalação manual e confirme `!menu`. Depois consulte
-[Implantação em servidor](../../DEPLOY.md) para manter o processo supervisionado.
-Não publique a pasta de sessão nem o arquivo de configuração ao mover o bot.
+> [!CAUTION]
+> Nunca publique esses arquivos. A sessão contém material de autenticação do
+> WhatsApp e os arquivos de configuração podem conter informações privadas ou
+> credenciais.
-## Socorro rápido
+Para diagnóstico adicional, rode `npm run preflight` e consulte **[Solução de problemas](../solucao-de-problemas.md)**.
-- **`sudo: command not found`:** sua distribuição usa outro método de
- administração; consulte a documentação dela ou peça ao administrador para
- instalar Git, Node.js e FFmpeg.
-- **Node mostra `v18`:** atualize para Node.js 22 ou 24 e reabra o Terminal.
-- **Permissão negada no instalador:** rode com `bash scripts/install-linux.sh`,
- exatamente como na etapa 6.
-- **A pasta `shogun` já existe:** use `cd ~/shogun`; não clone por cima.
-- **Ainda não funcionou:** rode `npm run preflight` e consulte
- [solução de problemas](../solucao-de-problemas.md).
+← Voltar à página principal
\ No newline at end of file
diff --git a/docs/instalacao/macos.md b/docs/instalacao/macos.md
new file mode 100644
index 0000000..3bbcf1a
--- /dev/null
+++ b/docs/instalacao/macos.md
@@ -0,0 +1,198 @@
+🍎 SHOGUN no macOS
+Intel ou Apple Silicon, do Terminal ao WhatsApp conectado.
+
+
+
+
+
+
+
+## Rota completa
+
+| Etapa | Ação | Sinal de sucesso |
+| --- | --- | --- |
+| **1** | instalar requisitos | versões aparecem no Terminal |
+| **2** | clonar o SHOGUN | pasta `~/shogun` criada |
+| **3** | executar instalador | setup concluído |
+| **4** | rodar preflight | requisitos obrigatórios aprovados |
+| **5** | conectar WhatsApp | feed ativo |
+| **6** | testar | `!menu` responde |
+
+---
+
+## 1 · Prepare o Mac
+
+Se usa Homebrew:
+
+```bash
+brew update
+brew install node git ffmpeg
+```
+
+Confirme:
+
+```bash
+node --version
+npm --version
+git --version
+ffmpeg -version
+```
+
+> [!IMPORTANT]
+> O SHOGUN exige **Node.js 20.19.0 ou superior**.
+
+Se `brew` não existir, instale os requisitos pelos distribuidores oficiais antes de continuar.
+
+---
+
+## 2 · Baixe o projeto
+
+```bash
+cd ~
+git clone https://github.com/dgreych/shogun.git
+cd shogun
+```
+
+Se a pasta já existir, use `cd ~/shogun` em vez de clonar novamente.
+
+---
+
+## 3 · Execute o instalador
+
+```bash
+bash scripts/install-macos.sh
+```
+
+O script cria `.env.local` a partir de `.env.example` se necessário, executa o
+preflight, instala as dependências travadas pelo `package-lock.json`, abre o
+setup e verifica as entradas principais do runtime. Um `.env.local` existente
+é preservado.
+
+
+| Seu nome | identificação do dono principal desta instância |
+| Número | país + DDD + número do dono principal, somente dígitos |
+| Nome do bot | nome desta instalação |
+| Prefixo | por exemplo ! |
+
+
+### Quem é o dono principal?
+
+```text
+projeto SHOGUN
+ └── sua instalação no macOS
+ ├── dono principal -> numerodono
+ ├── sessão WhatsApp
+ └── grupos -> administradores próprios
+```
+
+O número salvo pelo setup controla **esta instância** e seus comandos de dono.
+Ele não altera a autoria do projeto e não é a mesma coisa que ser administrador
+de um grupo.
+
+Veja **[Configuração da sua instância](../configuracao-da-instancia.md)** para a
+matriz de integrações e credenciais opcionais.
+
+---
+
+## 4 · Faça o preflight
+
+```bash
+npm run preflight
+```
+
+Esse comando confere os requisitos, a configuração do dono e o estado das
+integrações sem mostrar tokens. Avisos sobre APIs opcionais não impedem o núcleo
+quando esses recursos não estão sendo usados.
+
+> [!NOTE]
+> Nenhuma chave de API é necessária apenas para chegar ao primeiro `!menu`.
+
+---
+
+## 5 · Inicie e conecte
+
+```bash
+npm start
+```
+
+O painel oferece **QR Code** ou **código de pareamento**.
+
+
+
+
+
+Para QR Code, no telefone abra **WhatsApp → Aparelhos conectados → Conectar um aparelho**. Depois que a sessão for criada, os próximos boots reutilizam a sessão enquanto ela permanecer válida.
+
+---
+
+## 6 · Teste
+
+Em um grupo de teste:
+
+```text
+!menu
+```
+
+Se escolheu outro prefixo, use-o no lugar de `!`.
+
+---
+
+## APIs opcionais
+
+Depois de confirmar o núcleo, edite `.env.local` apenas para os recursos que
+quiser ativar:
+
+- BunnyFy: URL + credencial de consumidor e modos desejados;
+- NVIDIA direta: `NVIDIA_API_KEY` para IA direta/fallback;
+- VEX legado: `VEX_API_KEY` + `VEX_SITE` somente quando usado.
+
+Detalhes: **[Configuração da sua instância](../configuracao-da-instancia.md)**.
+
+---
+
+## Uso diário
+
+### Iniciar novamente
+
+```bash
+cd ~/shogun
+npm start
+```
+
+### Atualizar
+
+```bash
+cd ~/shogun
+git pull --ff-only
+npm ci --no-audit --no-fund
+npm run preflight
+npm start
+```
+
+### Diagnosticar
+
+```bash
+cd ~/shogun
+npm run preflight
+```
+
+---
+
+## 🔐 Proteja a instalação
+
+Arquivos privados:
+
+```text
+.env.local
+dados/src/config.json
+dados/database/qr-code/
+```
+
+> [!CAUTION]
+> Não publique, envie ou compartilhe esses arquivos. A sessão contém material
+> de autenticação da conta conectada e as configurações podem conter dados
+> privados ou credenciais.
+
+Para problemas adicionais, consulte **[Solução de problemas](../solucao-de-problemas.md)**.
+
+← Voltar à página principal
\ No newline at end of file
diff --git a/docs/instalacao/termux.md b/docs/instalacao/termux.md
index de5f90d..1c49575 100644
--- a/docs/instalacao/termux.md
+++ b/docs/instalacao/termux.md
@@ -1,422 +1,321 @@
-# Instalar o 𝖘𝖍𝖔𝖌𝖚𝖓 no Android
+📱 SHOGUN no Android
+Instalação pelo Termux, do zero ao primeiro !menu.
-Este guia começa do zero. Você não precisa conhecer programação nem ter usado
-um terminal antes. Faça uma etapa por vez e só passe para a próxima quando vir
-o resultado indicado.
-
-> **Tempo da primeira instalação:** normalmente 15 a 35 minutos. Use Wi-Fi,
-> deixe o aparelho carregando e reserve pelo menos 2 GB livres.
-
-### Como ler este guia
-
-Cada etapa tem um bloco assim, mostrando o que deve aparecer na sua tela:
+
+
+
+
+
-> ✅ **Deu certo se você vir:** uma descrição do que a tela mostra quando o
-> passo funcionou.
->
-> ⚠️ **Se aparecer outra coisa:** o erro mais comum daquele passo e o que fazer.
+> [!TIP]
+> Faça a primeira instalação no Wi‑Fi, com o aparelho carregando e pelo menos **2 GB livres**. O bot não exige Termux:API para manter o wake lock.
-**Copie e cole um comando por vez.** Espere ele terminar antes do próximo — a
-linha só volta a aceitar digitação quando o anterior acabou. Se a tela ficar
-parada com texto correndo, está trabalhando: aguarde.
+
+
+
-## O que você precisa antes de começar
+## Rota completa
-Você vai precisar de:
+| Etapa | O que acontece | Você sabe que deu certo quando… |
+| --- | --- | --- |
+| **1** | instalar o Termux | aparece o terminal com `$` |
+| **2** | atualizar pacotes | o prompt volta sem erro |
+| **3** | clonar o SHOGUN | `pwd` termina em `/shogun` |
+| **4** | rodar o instalador | aparece a configuração do bot |
+| **5** | manter o processo acordado | `termux-wake-lock` não retorna erro |
+| **6** | conectar o WhatsApp | o feed do SHOGUN começa a rodar |
+| **7** | testar | `!menu` recebe resposta |
-- um aparelho com Android 7 ou mais recente;
-- WhatsApp funcionando no número que será conectado;
-- internet estável durante a instalação;
-- o navegador do celular;
-- de preferência, um aparelho reserva para deixar o bot ligado direto.
+---
-O 𝖘𝖍𝖔𝖌𝖚𝖓 funciona como um aparelho conectado à sua conta. Quando possível,
-comece com um número e um grupo de testes antes de colocá-lo numa comunidade.
+## 1 · Instale o Termux
-## Etapa 1 — baixar o Termux verdadeiro
+Use preferencialmente uma das fontes oficiais da linha tradicional:
-O Termux é o aplicativo que abre a linha de comando onde o bot vai rodar.
-Não use cópias encontradas em sites de APK e não use a edição antiga da Play
-Store.
+- **[F-Droid](https://f-droid.org/packages/com.termux/)**
+- **[GitHub Releases](https://github.com/termux/termux-app/releases)**
-### Baixe o APK
+> [!IMPORTANT]
+> A edição do Google Play segue uma linha diferente e ainda pode variar em compatibilidade. Também não misture o aplicativo principal e plugins baixados de fontes diferentes, porque as assinaturas não combinam.
-Toque no link e o download começa. Os dois servem; o do F-Droid costuma estar
-numa versão mais nova.
+Abra o Termux. Se você vê uma linha terminando em `$`, está no lugar certo. É só um terminal. Ele parece hostil porque terminais foram desenhados antes de alguém descobrir bordas arredondadas.
-| Fonte | Link direto | Observação |
-| --- | --- | --- |
-| **F-Droid** (recomendado) | [Baixar Termux](https://f-droid.org/repo/com.termux_1022.apk) | Versão mais recente. Não precisa instalar a loja F-Droid |
-| **GitHub oficial** | [Baixar Termux](https://github.com/termux/termux-app/releases/download/v0.118.3/termux-app_v0.118.3%2Bgithub-debug_universal.apk) | Arquivo `universal`, funciona em qualquer aparelho |
+---
-Se algum link estiver fora do ar ou você quiser conferir se saiu versão nova,
-as páginas oficiais são
-[F-Droid](https://f-droid.org/packages/com.termux/) e
-[GitHub Releases](https://github.com/termux/termux-app/releases).
+## 2 · Atualize o ambiente
-> ⚠️ **Não baixe o Termux da Play Store.** A versão de lá está congelada há
-> anos e não funciona para isso. Também não use sites de APK genéricos.
+```bash
+pkg update -y && pkg upgrade -y
+```
-### Instalar o arquivo baixado
+**O que isso faz:** atualiza a lista de pacotes e instala as versões mais recentes disponíveis para o seu Termux.
-1. Abra o arquivo baixado. O Android costuma mostrar a notificação de download
- concluído — toque nela.
-2. Vai aparecer um aviso de que o navegador não tem permissão para instalar
- aplicativos. Toque em **Configurações**, ligue **Permitir desta fonte** e
- volte com o botão de voltar.
-3. Toque em **Instalar** e aguarde.
-4. Se quiser deixar o aparelho mais fechado, volte às configurações e desligue
- essa permissão depois da instalação.
+Se surgir uma pergunta sobre arquivo de configuração que você nunca alterou, a opção padrão costuma ser suficiente: pressione **Enter**.
-> Escolha uma fonte e permaneça nela. Termux, Termux:API e Termux:Boot precisam
-> vir todos do F-Droid ou todos do GitHub. Misturar fontes causa erro de
-> assinatura e o Android recusa a instalação dos complementos.
+---
-> ✅ **Deu certo se você vir:** o Termux abrindo numa tela preta com um cursor
-> piscando, e não uma tela de erro do Android.
->
-> ⚠️ **Se o app não instalar:** quase sempre é a permissão de "instalar apps
-> desconhecidos" desligada para o navegador. Volte ao passo da permissão.
+## 3 · Baixe o SHOGUN
-## Etapa 2 — conhecer a tela preta
+```bash
+cd ~
+git clone https://github.com/dgreych/shogun.git
+cd shogun
+```
-1. Abra o **Termux** pelo ícone recém-instalado.
-2. Na primeira abertura, aguarde a linha de texto com um sinal `$` aparecer.
- Esse sinal quer dizer: “pronto para receber um comando”.
-3. Para colar um comando, mantenha o dedo pressionado na tela e toque em
- **Paste/Colar**. Depois toque na tecla **Enter** do teclado.
+Confira onde está:
-Você sempre vai copiar **somente o conteúdo dentro da caixa**, uma caixa por
-vez. Não copie o sinal `$`, números de etapa ou explicações.
+```bash
+pwd
+```
-> ✅ **Deu certo se você vir:** uma linha terminada em `$` esperando você
-> digitar. Esse `$` é o convite: quando ele aparece, o Termux está livre.
->
-> ⚠️ **Se a tela ficar em branco:** toque nela uma vez. O teclado sobe e o
-> cursor volta.
+> [!NOTE]
+> Se aparecer `destination path 'shogun' already exists`, não clone de novo. Use `cd ~/shogun`.
-## Etapa 3 — atualizar o Termux
+---
-Cole este primeiro comando e pressione Enter:
+## 4 · Deixe o projeto preparar o aparelho
```bash
-pkg update -y
+bash scripts/install-termux.sh
```
-Várias linhas vão passar pela tela. Isso é normal. Aguarde até o `$` aparecer
-novamente. Se o Termux perguntar qual configuração manter, aceite a opção
-padrão pressionando Enter.
+O instalador do repositório faz a parte tediosa em ordem previsível:
-Agora instale a ferramenta que vai buscar o 𝖘𝖍𝖔𝖌𝖚𝖓:
+1. atualiza o índice do Termux;
+2. instala **Git, Node.js LTS, FFmpeg e termux-tools**;
+3. cria `.env.local` a partir de `.env.example` se ainda não existir;
+4. executa o preflight da plataforma;
+5. instala as dependências travadas em `package-lock.json` com `npm ci`;
+6. abre a configuração inicial.
-```bash
-pkg install -y git
-```
+Um `.env.local` existente é preservado. APIs opcionais começam desligadas.
-Espere o `$` voltar.
+### O setup pergunta
-> ✅ **Deu certo se você vir:** várias linhas correndo e, no fim, o `$` de
-> volta sem nenhuma linha começando com `E:` ou `Error`.
->
-> ⚠️ **Se pedir confirmação:** digite `y` e toque em Enter. É normal.
->
-> ⚠️ **Se travar em "Waiting for headers":** sua rede está instável. Aguarde
-> ou troque de Wi-Fi e rode o comando de novo — repetir não estraga nada.
+
+| Seu nome | como o bot identifica o dono principal desta instância |
+| Seu número | país + DDD + número do dono principal, somente dígitos |
+| Nome do bot | o nome exibido pela instalação |
+| Prefixo | por exemplo ! |
+
-## Etapa 4 — baixar o 𝖘𝖍𝖔𝖌𝖚𝖓
+A configuração é gravada localmente em `dados/src/config.json`.
-Cole:
+### Quem é o dono principal?
-```bash
-git clone https://github.com/dgreych/shogun.git
+```text
+projeto SHOGUN
+ └── sua instalação no Android
+ ├── dono principal -> numerodono
+ ├── sessão WhatsApp
+ └── grupos -> administradores próprios
```
-Quando aparecer `done` e o `$` voltar, entre na pasta que acabou de chegar:
+O número do setup controla **esta cópia do bot** e seus comandos de dono. Isso
+não muda os créditos/autoria do projeto e não é a mesma coisa que ser
+administrador de um grupo.
-```bash
-cd shogun
-```
+Veja **[Configuração da sua instância](../configuracao-da-instancia.md)** para
+entender APIs e credenciais antes de preencher qualquer chave.
+
+---
-O terminal não mostra uma animação ao entrar. Você pode confirmar o lugar com:
+## 5 · Impeça o Android de dormir em cima do bot
```bash
-pwd
+termux-wake-lock
```
-O fim da linha deve ser `/shogun`.
-
-> ✅ **Deu certo se você vir:** uma pasta nova chamada `shogun`. Confirme
-> digitando `ls` e Enter: o nome precisa aparecer na lista.
->
-> ⚠️ **Se disser "already exists":** a pasta já foi baixada antes. Entre nela
-> com `cd shogun` e siga.
-
-## Etapa 5 — preparar o bot
+O wake lock vem de `termux-tools`. **Não é necessário instalar o aplicativo Termux:API só para isso.**
-Cole:
+Se o comando estiver ausente:
```bash
-bash scripts/install-termux.sh
+pkg install -y termux-tools
+termux-wake-lock
```
-O instalador prepara Node.js, FFmpeg e os componentes do 𝖘𝖍𝖔𝖌𝖚𝖓. Pode parecer
-parado durante alguns minutos; não feche o Termux. A instalação chegou ao ponto
-certo quando aparecer o título **Quartel de configuração do 𝖘𝖍𝖔𝖌𝖚𝖓**.
-
-### Responder à configuração
+Também retire o Termux da otimização agressiva de bateria do fabricante. Android adora matar exatamente o processo que você queria manter vivo e chamar isso de otimização.
-O assistente faz quatro perguntas. Digite a resposta e pressione Enter em cada
-uma:
+---
-1. **Como o 𝖘𝖍𝖔𝖌𝖚𝖓 deve chamar você?** — por exemplo, `Mauricio`.
-2. **Seu número com país e DDD** — somente dígitos. Exemplo fictício:
- `5511999999999` (`55` do Brasil, DDD e número; sem `+`, espaço ou traço).
-3. **Nome do bot** — pressione Enter para manter `𝖘𝖍𝖔𝖌𝖚𝖓`.
-4. **Prefixo de comando** — pressione Enter para manter `!`.
+## 6 · Verifique e conecte
-Ao final, você deve ver **Configuração local salva** e **𝖘𝖍𝖔𝖌𝖚𝖓 pronto**. O
-arquivo com esses dados fica apenas no aparelho e não entra no repositório.
-
-Se digitou algo errado, não reinstale tudo. Rode:
+Antes do boot:
```bash
-npm run setup
+npm run preflight
```
-> ✅ **Deu certo se você vir:** as perguntas de configuração aparecendo uma a
-> uma, e no fim uma mensagem de conclusão.
->
-> ⚠️ **Se parar com erro de permissão:** feche o Termux por completo, abra de
-> novo e repita a partir do `cd shogun`.
+O diagnóstico verifica sistema, configuração, dono principal e integrações sem
+imprimir tokens. Avisos sobre APIs opcionais não impedem o núcleo quando você
+não usa aqueles recursos.
-## Etapa 6 — conferir se está tudo certo
-
-Cole:
+Depois:
```bash
-npm run preflight
+npm start
```
-Node.js, npm, Git e FFmpeg devem aparecer aprovados. Um aviso sobre
-`termux-wake-lock` não impede o primeiro teste; cuidaremos disso depois.
+O painel real da aplicação oferece:
-> ✅ **Deu certo se você vir:** uma lista de verificações com marcas de
-> aprovado, sem nenhuma linha vermelha.
+- **QR Code**;
+- **código de pareamento**;
+- sair.
-
+
-Imagem gerada da execução real do comando. O aviso amarelo sobre
-configuração é esperado antes da etapa seguinte.
->
-> ⚠️ **Se o FFmpeg aparecer como ausente:** rode `pkg install ffmpeg -y` e
-> repita a verificação.
+### QR Code
-## Etapa 7 — conectar o WhatsApp no mesmo celular
-
-Inicie o 𝖘𝖍𝖔𝖌𝖚𝖓:
-
-```bash
-npm start
-```
+No WhatsApp do número que será usado pelo bot, abra **Aparelhos conectados → Conectar um aparelho** e leia o QR mostrado no Termux.
-Na primeira vez, o terminal oferece três opções. Como WhatsApp e Termux estão
-no mesmo celular, digite `2` para **código de pareamento** e pressione Enter.
-Quando ele pedir o telefone, informe novamente país + DDD + número, somente
-dígitos.
+### Código de pareamento
-Um código curto aparecerá no terminal. Anote-o ou copie-o antes de sair da tela.
-Agora:
+Escolha a opção correspondente. O SHOGUN usa o número salvo no setup e pede ao WhatsApp um código para ser informado no fluxo de aparelhos conectados.
-1. abra o WhatsApp sem encerrar o Termux;
-2. toque no menu de três pontos;
-3. abra **Aparelhos conectados** ou **Dispositivos conectados**;
-4. toque em **Conectar um aparelho**;
-5. escolha **Conectar com número de telefone**;
-6. digite o código mostrado pelo 𝖘𝖍𝖔𝖌𝖚𝖓.
+> [!TIP]
+> Depois que a sessão é criada, inicializações futuras reutilizam essa sessão enquanto ela permanecer válida. Novo QR em todo boot não é comportamento normal.
-Volte ao Termux. Aguarde a confirmação de conexão. Na próxima abertura, essa
-sessão será reconhecida automaticamente e não será necessário parear de novo.
+---
-### Se o WhatsApp estiver em outro aparelho
+## 7 · Faça o teste que importa
-Digite `1` para usar QR Code. No aparelho que tem o WhatsApp, abra **Aparelhos
-conectados → Conectar um aparelho** e leia o QR mostrado no Termux.
-
-> ✅ **Deu certo se você vir:** a palavra **CONECTADO** na tela e, logo
-> depois, uma mensagem chegando no WhatsApp do dono.
-
-
-
-
+Quando o feed aparecer:
-
+
->
-> ⚠️ **Se o QR sumir antes de você ler:** ele expira em segundos e é gerado de
-> novo sozinho. Deixe a câmera pronta antes de olhar a tela.
->
-> ⚠️ **Se pedir o QR toda vez que reinicia:** a sessão não está sendo salva.
-> Confirme que você não apagou a pasta `dados/database`.
-
-## Etapa 8 — o primeiro comando
-Com o 𝖘𝖍𝖔𝖌𝖚𝖓 conectado, abra uma conversa de teste no WhatsApp e envie:
+Envie em um grupo de teste:
```text
!menu
```
-Se o menu chegou, a instalação está concluída. Antes de dar cargo de
-administrador, teste comandos simples e leia o guia de
-[primeiros passos](../primeiros-passos.md).
+Se escolheu outro prefixo, troque `!` por ele.
-> ✅ **Deu certo se você vir:** o bot respondendo o `!menu` no grupo, com a
-> lista de comandos.
->
-> ⚠️ **Se ele não responder:** confirme que está no grupo, que o prefixo é o
-> mesmo que você configurou, e que a tela do Termux ainda mostra o bot ligado.
+**Recebeu resposta?** Instalação, conexão e roteamento básico estão funcionando.
-## Etapa 9 — impedir que o Android desligue o bot
+---
-O Android economiza bateria fechando aplicativos em segundo plano. Para o
-𝖘𝖍𝖔𝖌𝖚𝖓 permanecer conectado, faça as duas proteções abaixo.
+## APIs opcionais
-### Retirar a otimização de bateria
+Você não precisa de uma chave de API para chegar ao primeiro `!menu`.
-Nas configurações do Android, procure **Aplicativos → Termux → Bateria** e
-escolha algo como **Sem restrições**, **Não otimizar** ou **Permitir atividade
-em segundo plano**. O nome muda conforme a marca do celular.
+Depois que o núcleo estiver funcionando, edite `.env.local` somente para os
+recursos que quiser ativar:
-### Ativar o bloqueio de suspensão
+- BunnyFy: URL + credencial de consumidor;
+- NVIDIA direta: `NVIDIA_API_KEY`;
+- VEX legado: `VEX_API_KEY` + `VEX_SITE`.
-Instale o aplicativo **Termux:API** pela mesma fonte usada para o Termux. Depois
-abra o Termux e rode:
+A matriz completa e os modos `off`, `primary` e `exclusive` estão em
+**[Configuração da sua instância](../configuracao-da-instancia.md)**.
-```bash
-pkg install -y termux-api
-```
+---
-Ative a proteção:
+## Da próxima vez
```bash
+cd ~/shogun
termux-wake-lock
+npm start
```
-Se o Android pedir confirmação, permita. Para liberar a proteção quando o bot
-estiver parado, use:
+## Atualizar sem destruir a sessão
+
+Pare o processo com `Ctrl + C` e execute:
```bash
-termux-wake-unlock
+cd ~/shogun
+git pull --ff-only
+npm ci --no-audit --no-fund
+npm run preflight
+npm start
```
-## Sua rotina depois da instalação
-
-### Abrir o 𝖘𝖍𝖔𝖌𝖚𝖓 outro dia
+`npm ci` respeita o lockfile do projeto. O diretório de sessão não deve ser apagado durante uma atualização normal.
-Abra o Termux e use, um por vez:
+---
-```bash
-cd shogun
-```
+## Diagnóstico rápido
```bash
-termux-wake-lock
-```
-
-```bash
-npm start
+cd ~/shogun
+npm run preflight
```
-### Parar com segurança
+O preflight verifica **Node.js, npm, Git, FFmpeg, Termux, configuração, dono da
+instância, dependências locais e estado das integrações opcionais**.
-Volte ao Termux e pressione `Ctrl+C`. Na fileira extra do Termux, toque em
-`CTRL` e depois na letra `C`. Quando o `$` reaparecer, o processo parou.
-
-### Atualizar o 𝖘𝖍𝖔𝖌𝖚𝖓
-
-Pare o bot com `Ctrl+C`, confirme que está na pasta `shogun` e rode:
+
+Node.js está abaixo de 20.19
+
```bash
-git pull --ff-only
+pkg update -y
+pkg upgrade -y
+pkg install -y nodejs-lts
+node --version
```
+
-```bash
-npm ci --no-audit --no-fund
-```
+
+FFmpeg não existe
+
```bash
-npm start
+pkg install -y ffmpeg
+ffmpeg -version
```
+
-Não apague `dados/database/qr-code/`: essa pasta contém a sessão conectada.
-
-## Opcional — iniciar após reiniciar o celular
-
-Só faça isto depois de ter iniciado manualmente, conectado o WhatsApp e
-confirmado `!menu`.
-
-1. Instale **Termux:Boot** pela mesma fonte usada para o Termux.
-2. Toque uma vez no ícone **Termux:Boot**. Essa primeira abertura autoriza o
- complemento a agir no próximo reinício.
-3. Volte ao Termux e instale o editor usado nesta etapa:
+
+npm ci falhou depois de uma instalação antiga
+
```bash
-pkg install -y nano
+cd ~/shogun
+rm -rf node_modules
+npm ci --no-audit --no-fund
```
+
-4. Crie a pasta:
+
+Quero refazer a configuração
+
```bash
-mkdir -p ~/.termux/boot
+cd ~/shogun
+npm run setup
```
+
-5. Abra o arquivo:
+---
-```bash
-nano ~/.termux/boot/start-shogun
-```
+## 🔐 A parte que você não deve mandar para ninguém
-6. Cole exatamente:
+Arquivos privados desta instalação:
-```bash
-#!/data/data/com.termux/files/usr/bin/bash
-termux-wake-lock
-cd "$HOME/shogun"
-npm start >> "$HOME/shogun/termux-boot.log" 2>&1
+```text
+.env.local
+dados/src/config.json
+dados/database/qr-code/
```
-7. Salve tocando em `CTRL`, depois `O`, Enter, `CTRL` e `X`.
-8. Torne o arquivo executável:
+> [!CAUTION]
+> Esses arquivos podem representar acesso à sessão vinculada ou conter
+> configuração privada. **Não envie, não coloque em Drive, não publique em
+> GitHub e não cole seu conteúdo em suporte.**
-```bash
-chmod 700 ~/.termux/boot/start-shogun
-```
+## O mínimo obrigatório
-O 𝖘𝖍𝖔𝖌𝖚𝖓 não altera `.bashrc` e não ativa isso sozinho. Para conferir depois
-de reiniciar, abra o Termux e veja:
+**Necessário para o núcleo:** Android + Termux, Node.js 20.19+, npm, Git, FFmpeg, configuração do dono e conexão com WhatsApp.
-```bash
-tail -n 40 ~/shogun/termux-boot.log
-```
+**Opcional:** integrações externas, IA e capacidades que dependam de APIs específicas. A ausência delas não impede o núcleo do SHOGUN de iniciar e conectar.
+
+---
-## Socorro rápido
-
-- **`pkg` ou downloads falham:** troque de Wi-Fi/dados móveis, reabra o Termux
- e repita apenas o comando que falhou.
-- **`cd: shogun: No such file or directory`:** o download não terminou ou você
- está em outro lugar. Rode `cd`, depois repita a etapa 4.
-- **O instalador parece parado:** mantenha o aplicativo aberto; a primeira
- instalação pode levar vários minutos.
-- **O código expirou:** pare com `Ctrl+C`, rode `npm start` e gere outro.
-- **O bot cai com a tela apagada:** revise a bateria, rode
- `termux-wake-lock` e mantenha a notificação do Termux ativa.
-- **Termux:API ou Termux:Boot não instala:** provavelmente as fontes foram
- misturadas. Todos os aplicativos Termux precisam vir da mesma fonte.
-- **Apareceu um erro que você não entende:** rode `npm run preflight` e procure
- a seção correspondente em [solução de problemas](../solucao-de-problemas.md).
-
-Nunca envie a pasta de sessão, seu código de pareamento ou uma captura contendo
-credenciais. Para pedir ajuda, copie apenas a mensagem de erro.
+← Voltar à página principal
\ No newline at end of file
diff --git a/docs/instalacao/windows.md b/docs/instalacao/windows.md
index a6e92ba..ed598fc 100644
--- a/docs/instalacao/windows.md
+++ b/docs/instalacao/windows.md
@@ -1,198 +1,274 @@
-# SHOGUN no Windows: instalação para iniciantes
+🪟 SHOGUN no Windows
+Do PowerShell ao primeiro !menu, sem pular as partes que normalmente viram problema depois.
-Este caminho serve para Windows 10 e 11. Reserve cerca de 20 minutos, use uma
-conta que possa instalar programas e mantenha o computador conectado à internet.
+
+
+
+
+
+
+## Rota completa
+
+| Etapa | Ação | Sinal de sucesso |
+| --- | --- | --- |
+| **1** | abrir PowerShell | prompt disponível |
+| **2** | instalar Git, Node e FFmpeg | comandos mostram versões |
+| **3** | clonar o projeto | pasta `shogun` criada |
+| **4** | rodar o instalador | setup concluído |
+| **5** | executar preflight | requisitos obrigatórios aprovados |
+| **6** | conectar WhatsApp | feed do bot ativo |
+| **7** | testar | `!menu` responde |
+
+---
-## Etapa 1 — abrir o PowerShell
+## 1 · Abra o PowerShell
-1. Abra o menu **Iniciar**.
-2. Digite `PowerShell`.
-3. Abra **Windows PowerShell**. Não é necessário abrir como administrador para
- rodar o SHOGUN.
+No menu **Iniciar**, procure por **PowerShell**. Para executar o SHOGUN normalmente não é necessário abrir como administrador.
-Você vai copiar uma caixa por vez, colar com o botão direito ou `Ctrl+V` e
-pressionar Enter. Espere o cursor voltar antes de seguir.
+Você pode colar os comandos com `Ctrl+V`. Execute um bloco de cada vez e espere o prompt voltar.
-## Etapa 2 — instalar as ferramentas
+---
-O Windows 10/11 atualizado inclui o `winget`, instalador oficial do sistema.
-Confira:
+## 2 · Instale as ferramentas
+
+Primeiro confira se o `winget` está disponível:
```powershell
winget --version
```
-Se o comando não existir, instale ou atualize **Instalador de Aplicativo** pela
-Microsoft Store, feche o PowerShell e abra-o de novo.
+Se não estiver, atualize o **Instalador de Aplicativo** pela Microsoft Store e abra uma nova janela do PowerShell.
-Instale o Git:
+### Git
```powershell
winget install --id Git.Git -e --source winget
```
-Instale o Node.js LTS:
+### Node.js LTS
```powershell
winget install --id OpenJS.NodeJS.LTS -e --source winget
```
-Instale o FFmpeg:
+### FFmpeg
```powershell
winget install --id Gyan.FFmpeg -e --source winget
```
-Aceite os termos quando o Windows pedir. Feche completamente o PowerShell e
-abra uma janela nova para que os novos comandos sejam reconhecidos.
-
-Se preferir instalar clicando, use somente as páginas oficiais de
-[Node.js](https://nodejs.org/en/download),
-[Git](https://git-scm.com/install/windows) e
-[FFmpeg](https://ffmpeg.org/download.html). Escolha Node.js 22 ou 24 LTS e
-lembre-se de adicionar FFmpeg ao `PATH`.
+Feche o PowerShell e abra outro depois das instalações. O Windows precisa reconstruir o `PATH`, porque descobrir imediatamente que um programa acabou de ser instalado seria aparentemente pedir demais.
-## Etapa 3 — conferir o terreno
-
-Na janela nova, rode um por vez:
+### Confira
```powershell
node --version
-```
-
-```powershell
npm --version
-```
-
-```powershell
git --version
-```
-
-```powershell
ffmpeg -version
```
-Cada comando deve mostrar uma versão. Se algum disser “não é reconhecido”,
-reinicie o computador uma vez antes de reinstalar.
+> [!IMPORTANT]
+> O SHOGUN exige **Node.js 20.19.0 ou superior**. Node 22/24 LTS é uma boa escolha para instalação nova.
-## Etapa 4 — baixar o SHOGUN
+---
-Escolha uma pasta simples. Este comando vai para sua pasta de usuário:
+## 3 · Baixe o projeto
```powershell
Set-Location $HOME
-```
-
-Baixe o projeto:
-
-```powershell
git clone https://github.com/dgreych/shogun.git
+Set-Location shogun
```
-Entre no quartel:
+Se a pasta já existir, não clone por cima:
```powershell
-Set-Location shogun
+Set-Location "$HOME\shogun"
```
-## Etapa 5 — preparar e configurar
+---
+
+## 4 · Execute o instalador do SHOGUN
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-windows.ps1
```
-O preparo baixa os componentes do bot e depois faz quatro perguntas:
+O instalador verifica o ambiente, cria `.env.local` a partir de `.env.example`
+se ele ainda não existir, instala as dependências travadas pelo projeto e abre
+o setup. Integrações externas ficam desligadas por padrão.
-1. como o SHOGUN deve chamar você;
-2. seu número com país e DDD, somente dígitos — exemplo fictício
- `5511999999999`;
-3. nome do bot — pressione Enter para manter `SHOGUN`;
-4. prefixo — pressione Enter para manter `!`.
+
+| Seu nome | identificação do dono principal desta instância |
+| Número | país + DDD + número do dono principal, somente dígitos |
+| Nome do bot | nome desta instalação |
+| Prefixo | por exemplo ! |
+
-Espere aparecer **SHOGUN pronto**. Para corrigir uma resposta depois, use:
+### O que “dono” significa aqui
+
+```text
+projeto SHOGUN
+ └── sua instalação no Windows
+ ├── dono principal -> número informado no setup
+ ├── sessão WhatsApp
+ └── grupos -> administradores próprios
+```
+
+O número configurado é o **dono principal desta cópia do bot** e recebe os
+privilégios reservados ao dono. Isso não muda a autoria do projeto e não é a
+mesma coisa que ser administrador de um grupo do WhatsApp.
+
+Para refazer apenas essa configuração depois:
```powershell
npm run setup
```
-## Etapa 6 — inspeção e conexão
+Para entender BunnyFy, NVIDIA, VEX e quais chaves são opcionais, consulte
+**[Configuração da sua instância](../configuracao-da-instancia.md)**.
+
+---
+
+## 5 · Faça a inspeção antes do boot
```powershell
npm run preflight
```
-
+
-Imagem gerada da execução real do comando. O aviso amarelo sobre
-configuração é esperado antes da etapa seguinte.
+O preflight verifica Node.js, npm, Git, FFmpeg, arquivos locais, dono principal
+e o estado das integrações sem imprimir tokens. Avisos sobre APIs opcionais não
+impedem o núcleo de iniciar quando esses recursos não estão sendo usados.
+
+> [!NOTE]
+> Você não precisa de uma chave de API para chegar ao primeiro `!menu`. Configure
+> integrações externas depois que o núcleo estiver funcionando.
-Se todos os itens obrigatórios estiverem aprovados, inicie:
+---
+
+## 6 · Inicie e conecte o WhatsApp
```powershell
npm start
```
-Escolha `1` para QR Code. No telefone, abra WhatsApp → menu de três pontos →
-**Aparelhos conectados/Dispositivos conectados → Conectar um aparelho** e leia
-o QR da tela do computador.
+O painel oferece **QR Code** e **código de pareamento**.
-Se a câmera não puder ler a tela, pare com `Ctrl+C`, inicie novamente, escolha
-`2` e siga o código de pareamento.
+
+
+
+
+Para QR Code, no telefone abra **WhatsApp → Aparelhos conectados → Conectar um aparelho**.
+
+Se preferir não usar a câmera, reinicie o fluxo e escolha o código de pareamento. Depois que a sessão for criada, ela é reaproveitada nos próximos boots enquanto continuar válida.
-## Etapa 7 — confirmar a primeira missão
+---
-Numa conversa de teste, envie:
+## 7 · Prove que terminou
+
+Envie em um grupo de teste:
```text
!menu
```
-Recebeu o menu? O posto está pronto. O PowerShell precisa permanecer aberto
-enquanto o SHOGUN estiver em serviço.
+Se escolheu outro prefixo, use-o no lugar de `!`.
-## Sua rotina
+> [!TIP]
+> O PowerShell precisa permanecer aberto enquanto o processo estiver rodando. Fechar a janela encerra essa execução do bot.
-Para parar com segurança, clique no PowerShell e pressione `Ctrl+C`.
+---
-Para voltar outro dia:
+## APIs opcionais
-```powershell
-Set-Location "$HOME\shogun"
-```
+Depois que `!menu` estiver funcionando, abra `.env.local` somente se quiser
+ativar recursos externos. A matriz atual está em
+**[Configuração da sua instância](../configuracao-da-instancia.md)**.
+
+Resumo:
+
+- núcleo + WhatsApp: sem chave de API;
+- BunnyFy: URL + credencial de consumidor quando ativada;
+- NVIDIA direta: `NVIDIA_API_KEY` apenas para IA direta/fallback;
+- VEX: `VEX_API_KEY` + `VEX_SITE` apenas no fallback legado correspondente.
+
+Nunca coloque chaves internas de providers da BunnyFy no bot.
+
+---
+
+## Uso diário
+
+### Parar
+
+Pressione `Ctrl+C`.
+
+### Iniciar novamente
```powershell
+Set-Location "$HOME\shogun"
npm start
```
-Para atualizar, pare o bot e rode:
+### Atualizar
```powershell
+Set-Location "$HOME\shogun"
git pull --ff-only
-```
-
-```powershell
npm ci --no-audit --no-fund
+npm run preflight
+npm start
```
-```powershell
-npm start
+---
+
+## Diagnóstico
+
+
+PowerShell diz que execução de scripts foi desabilitada
+
+Use o comando do instalador exatamente com -ExecutionPolicy Bypass. Ele aplica a exceção ao processo usado para essa instalação.
+
+
+
+ffmpeg, node ou git não é reconhecido
+
+Feche todas as janelas do PowerShell, abra outra e teste novamente. Se persistir, reinicie o Windows uma vez e rode npm run preflight antes de reinstalar qualquer coisa.
+
+
+
+O QR ficou pequeno ou ilegível
+
+Maximize a janela ou use o código de pareamento.
+
+
+
+A pasta shogun já existe
+
+Entre nela com Set-Location "$HOME\shogun". Não clone outra cópia por cima.
+
+
+---
+
+## 🔐 Proteja a instalação
+
+Arquivos privados importantes:
+
+```text
+.env.local
+dados/src/config.json
+dados/database/qr-code/
```
-Não copie apenas `node_modules` para outro PC. Em uma máquina nova, clone o
-projeto e execute o instalador novamente. Preserve com cuidado a pasta local de
-sessão; ela vale como uma chave do WhatsApp.
+> [!CAUTION]
+> Não envie esses arquivos para suporte, GitHub, Drive ou outras pessoas. A
+> sessão contém material de autenticação e os demais podem conter configuração
+> privada ou credenciais.
-## Socorro rápido
+Para diagnóstico adicional, execute `npm run preflight` e consulte **[Solução de problemas](../solucao-de-problemas.md)**.
-- **Execução de scripts foi desabilitada:** use exatamente o comando com
- `-ExecutionPolicy Bypass` mostrado na etapa 5; ele vale apenas para esse
- instalador.
-- **`ffmpeg` não é reconhecido:** feche todas as janelas do PowerShell e abra
- outra. Se persistir, reinstale o FFmpeg e reinicie o Windows.
-- **O QR ficou pequeno:** maximize a janela ou use código de pareamento.
-- **A pasta `shogun` já existe:** entre nela com `Set-Location shogun`; não
- clone por cima.
-- **Ainda não funcionou:** rode `npm run preflight` e consulte
- [solução de problemas](../solucao-de-problemas.md).
+← Voltar à página principal
\ No newline at end of file
diff --git a/docs/portabilidade.md b/docs/portabilidade.md
new file mode 100644
index 0000000..5e62be5
--- /dev/null
+++ b/docs/portabilidade.md
@@ -0,0 +1,25 @@
+# Portabilidade do SHOGUN
+
+O repositório público é tratado como uma distribuição instalável, não como uma fotografia ornamental do código.
+
+| Plataforma | Entrada recomendada | Validação automatizada |
+| --- | --- | --- |
+| Android / Termux | `bash scripts/install-termux.sh` | contrato Termux + Node 20.19 |
+| Windows 10/11 | `scripts/install-windows.ps1` | Windows + Node 20.19/22/24 |
+| Linux | `bash scripts/install-linux.sh` | Ubuntu + Node 20.19/22/24 |
+| macOS | `bash scripts/install-macos.sh` | macOS + Node 20.19/22/24 |
+
+## O que “suportado” significa
+
+A matriz verifica instalação de dependências, entradas principais do runtime, build modular e requisitos de plataforma. O job de qualidade executa também os testes de domínio, roteamento, estado, compatibilidade, regressões e validação de release.
+
+No Android, o GitHub Actions não finge ser um aparelho físico. O contrato automatizado valida o caminho específico do Termux, enquanto o guia documenta as etapas que dependem do dispositivo e do pareamento real com o WhatsApp.
+
+## Guias
+
+- [Termux](instalacao/termux.md)
+- [Windows](instalacao/windows.md)
+- [Linux](instalacao/linux.md)
+- [macOS](instalacao/macos.md)
+
+Para a primeira conexão e os cuidados com a sessão, veja também [Primeiros passos](primeiros-passos.md) e [Segurança](seguranca.md).
diff --git a/docs/primeiros-passos.md b/docs/primeiros-passos.md
index 98d29b4..71ad6c9 100644
--- a/docs/primeiros-passos.md
+++ b/docs/primeiros-passos.md
@@ -1,5 +1,25 @@
# Primeiros passos com o SHOGUN
+## Antes de tudo: esta instalação é sua instância
+
+Ao executar o setup, o número salvo em `numerodono` vira o **dono principal
+desta instância** do SHOGUN. É esse número que recebe privilégios reservados ao
+dono do bot.
+
+Isso não altera a autoria do projeto e também não é a mesma coisa que ser
+administrador de um grupo do WhatsApp.
+
+```text
+projeto SHOGUN
+ └── sua instalação
+ ├── dono principal -> numerodono
+ ├── sessão WhatsApp
+ └── grupos -> administradores próprios
+```
+
+Leia **[Configuração da sua instância](configuracao-da-instancia.md)** para a
+matriz completa de APIs e credenciais opcionais.
+
## A inspeção
Dentro da pasta do projeto:
@@ -8,7 +28,12 @@ Dentro da pasta do projeto:
npm run preflight
```
-Todos os itens obrigatórios devem aparecer com `✅`.
+Todos os itens obrigatórios devem aparecer com `✅`. Avisos sobre NVIDIA, VEX,
+BunnyFy ou outras integrações opcionais não impedem o núcleo de iniciar quando
+esses recursos não estão sendo usados.
+
+O preflight também verifica `.env.local` e a configuração da instância sem
+imprimir tokens ou chaves.
## A configuração
@@ -16,7 +41,12 @@ Todos os itens obrigatórios devem aparecer com `✅`.
npm run setup
```
-Informe seu nome, número com país e DDD, nome do bot e prefixo. Para manter um valor existente, apenas pressione Enter.
+Informe seu nome, número com país e DDD, nome do bot e prefixo. Para manter um
+valor existente, apenas pressione Enter.
+
+O setup grava `dados/src/config.json`. Os instaladores oficiais também criam
+`.env.local` a partir de `.env.example` quando necessário, mantendo APIs
+opcionais desligadas por padrão.
## A conexão
@@ -24,7 +54,8 @@ Informe seu nome, número com país e DDD, nome do bot e prefixo. Para manter um
npm start
```
-Escolha QR ou código de pareamento. A sessão fica local e será reutilizada no próximo início.
+Escolha QR ou código de pareamento. A sessão fica local e será reutilizada no
+próximo início.
## A primeira missão
@@ -34,11 +65,24 @@ Envie:
!menu
```
-Depois teste uma ferramenta simples, uma figurinha e um comando de grupo sem efeito destrutivo. Só dê cargo de administrador quando entender quais rotinas serão usadas.
+Depois teste uma ferramenta simples, uma figurinha e um comando de grupo sem
+efeito destrutivo. Só dê cargo de administrador quando entender quais rotinas
+serão usadas.
+
+## APIs vêm depois do primeiro boot
+
+O núcleo do SHOGUN funciona **sem chave de API obrigatória**. Você não precisa
+configurar BunnyFy, NVIDIA, VEX ou upload externo para provar que a instalação
+básica está correta. Primeiro confirme `!menu`; depois ative somente as
+integrações que realmente quiser usar.
+
+Consulte **[Configuração da sua instância](configuracao-da-instancia.md)** para
+saber exatamente qual variável pertence a cada capacidade.
## A rotina
- `Ctrl+C` encerra com segurança;
- `npm start` retoma a patrulha;
-- `npm run setup` altera a configuração;
+- `npm run setup` altera dono, nome e prefixo;
+- `npm run preflight` diagnostica sistema, configuração e integrações;
- `git pull --ff-only` e `npm ci` atualizam o código e as dependências.
diff --git a/scripts/install-linux.sh b/scripts/install-linux.sh
index 5b385ab..6e5eb51 100755
--- a/scripts/install-linux.sh
+++ b/scripts/install-linux.sh
@@ -10,6 +10,15 @@ for command in git node npm ffmpeg; do
fi
done
+if [ ! -f .env.local ]; then
+ cp .env.example .env.local
+ chmod 600 .env.local
+ echo "🔐 .env.local criado a partir de .env.example. APIs opcionais continuam desativadas."
+else
+ chmod 600 .env.local 2>/dev/null || true
+ echo "🔐 .env.local existente preservado."
+fi
+
node scripts/preflight-platform.mjs
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0=url.https://github.com/.insteadOf \
@@ -17,4 +26,4 @@ GIT_CONFIG_VALUE_0=ssh://git@github.com/ \
npm ci --no-audit --no-fund
npm run setup
-echo "SHOGUN pronto. Inicie com: npm start"
+echo "SHOGUN pronto. Rode 'npm run preflight' e depois 'npm start'."
diff --git a/scripts/install-macos.sh b/scripts/install-macos.sh
new file mode 100644
index 0000000..08e9dbf
--- /dev/null
+++ b/scripts/install-macos.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+missing=0
+for cmd in git node npm ffmpeg; do
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ echo "Falta o comando: $cmd"
+ missing=1
+ fi
+done
+
+if [ "$missing" -ne 0 ]; then
+ echo "Instale os requisitos e rode novamente. Com Homebrew: brew install node git ffmpeg"
+ exit 1
+fi
+
+if [ ! -f .env.local ]; then
+ cp .env.example .env.local
+ chmod 600 .env.local
+ echo "🔐 .env.local criado a partir de .env.example. APIs opcionais continuam desativadas."
+else
+ chmod 600 .env.local 2>/dev/null || true
+ echo "🔐 .env.local existente preservado."
+fi
+
+node scripts/preflight-platform.mjs
+
+GIT_CONFIG_COUNT=1 \
+GIT_CONFIG_KEY_0=url.https://github.com/.insteadOf \
+GIT_CONFIG_VALUE_0=ssh://git@github.com/ \
+npm ci --no-audit --no-fund
+
+npm run setup
+
+node --check dados/src/.scripts/start-v9-fixed.js
+node --check dados/src/connect.js
+
+echo "SHOGUN pronto. Rode 'npm run preflight' e depois 'npm start'."
diff --git a/scripts/install-termux.sh b/scripts/install-termux.sh
index 28fbcb4..805bb2e 100755
--- a/scripts/install-termux.sh
+++ b/scripts/install-termux.sh
@@ -4,7 +4,16 @@ set -euo pipefail
cd "$(dirname "$0")/.."
pkg update -y
-pkg install -y git nodejs-lts ffmpeg
+pkg install -y git nodejs-lts ffmpeg termux-tools
+
+if [ ! -f .env.local ]; then
+ cp .env.example .env.local
+ chmod 600 .env.local
+ echo "🔐 .env.local criado a partir de .env.example. APIs opcionais continuam desativadas."
+else
+ chmod 600 .env.local 2>/dev/null || true
+ echo "🔐 .env.local existente preservado."
+fi
node scripts/preflight-platform.mjs
@@ -15,4 +24,7 @@ npm ci --no-audit --no-fund
npm run setup
-echo "SHOGUN pronto. Use termux-wake-lock e depois npm start."
+node --check dados/src/.scripts/start-v9-fixed.js
+node --check dados/src/connect.js
+
+echo "SHOGUN pronto. Execute termux-wake-lock, rode 'npm run preflight' e depois 'npm start'."
diff --git a/scripts/install-windows.ps1 b/scripts/install-windows.ps1
index 9c213b7..4b9a4c1 100644
--- a/scripts/install-windows.ps1
+++ b/scripts/install-windows.ps1
@@ -7,6 +7,14 @@ foreach ($command in @("git", "node", "npm", "ffmpeg")) {
}
}
+if (-not (Test-Path ".env.local")) {
+ Copy-Item ".env.example" ".env.local"
+ Write-Host "🔐 .env.local criado a partir de .env.example. APIs opcionais continuam desativadas." -ForegroundColor Cyan
+}
+else {
+ Write-Host "🔐 .env.local existente preservado." -ForegroundColor Cyan
+}
+
node scripts/preflight-platform.mjs
$previousGitConfigCount = $env:GIT_CONFIG_COUNT
@@ -25,4 +33,4 @@ finally {
$env:GIT_CONFIG_VALUE_0 = $previousGitConfigValue
}
-Write-Host "SHOGUN pronto. Inicie com: npm start" -ForegroundColor Green
+Write-Host "SHOGUN pronto. Rode 'npm run preflight' e depois 'npm start'." -ForegroundColor Green
diff --git a/scripts/preflight-platform.mjs b/scripts/preflight-platform.mjs
index b405dfe..1303592 100755
--- a/scripts/preflight-platform.mjs
+++ b/scripts/preflight-platform.mjs
@@ -4,6 +4,8 @@ import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
+import { loadLocalEnv } from '../dados/src/.scripts/envLoader.js';
+
const ROOT = path.resolve(import.meta.dirname, '..');
const MIN_NODE = [20, 19, 0];
const failures = [];
@@ -32,11 +34,126 @@ function probe(label, command, args, required = true) {
return false;
}
+function isTrue(value) {
+ return ['true', '1', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
+}
+
+function isPresent(value) {
+ return Boolean(String(value || '').trim());
+}
+
+function maskedState(value) {
+ return isPresent(value) ? 'configurado' : 'não configurado';
+}
+
+function readLocalConfig(configPath) {
+ if (!fs.existsSync(configPath)) return null;
+ try {
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
+ } catch (error) {
+ failures.push(`dados/src/config.json não pôde ser lido: ${error.message}`);
+ return null;
+ }
+}
+
+function inspectInstanceConfig(config) {
+ if (!config) return;
+
+ const ownerName = String(config.nomedono || '').trim();
+ const ownerNumber = String(config.numerodono || '').replace(/\D/g, '');
+ const botName = String(config.nomebot || '').trim();
+ const prefix = String(config.prefixo || '').trim();
+
+ if (!ownerName) failures.push('O nome do dono principal da instância está vazio; execute npm run setup.');
+ if (!/^\d{10,15}$/.test(ownerNumber)) {
+ failures.push('O número do dono principal da instância deve ter 10 a 15 dígitos; execute npm run setup.');
+ }
+ if (!botName) failures.push('O nome local do bot está vazio; execute npm run setup.');
+ if (!prefix) failures.push('O prefixo de comandos está vazio; execute npm run setup.');
+
+ if (ownerName && /^\d{10,15}$/.test(ownerNumber) && botName && prefix) {
+ console.log('✅ Dono principal da instância — configurado');
+ console.log('✅ Nome do bot e prefixo — configurados');
+ }
+}
+
+function inspectOptionalIntegrations() {
+ console.log('\n🔌 Integrações opcionais\n');
+
+ const modeKeys = [
+ 'BUNNYFY_AI_MODE',
+ 'BUNNYFY_YOUTUBE_MODE',
+ 'BUNNYFY_TRANSCRIPTION_MODE',
+ 'BUNNYFY_FACEBOOK_MODE',
+ 'BUNNYFY_PINTEREST_MODE',
+ 'BUNNYFY_TIKTOK_MODE',
+ 'BUNNYFY_KWAI_MODE',
+ 'BUNNYFY_IMAGES_MODE',
+ 'BUNNYFY_STICKERS_MODE',
+ 'BUNNYFY_CANVAS_MODE',
+ 'BUNNYFY_LOGOS_MODE',
+ 'BUNNYFY_GAMES_MODE',
+ 'BUNNYFY_IMAGE_GEN_MODE',
+ 'BUNNYFY_TAVERN_RENDER_MODE',
+ 'BUNNYFY_NEXO_RENDER_MODE'
+ ];
+ const allowedModes = new Set(['off', 'primary', 'exclusive']);
+ const modes = modeKeys.map(key => [key, String(process.env[key] || 'off').trim().toLowerCase()]);
+
+ for (const [key, value] of modes) {
+ if (!allowedModes.has(value)) failures.push(`${key} possui modo inválido: use off, primary ou exclusive.`);
+ }
+
+ const bunnyFyEnabled = isTrue(process.env.BUNNYFY_ENABLED);
+ const activeModes = modes.filter(([, value]) => value !== 'off');
+ const bunnyFyBaseUrl = String(process.env.BUNNYFY_BASE_URL || '').trim();
+ const bunnyFyToken = String(process.env.BUNNYFY_API_TOKEN || '').trim();
+
+ if (!bunnyFyEnabled) {
+ console.log('ℹ️ BunnyFy — desativada; núcleo e fallbacks disponíveis continuam independentes');
+ } else if (!activeModes.length) {
+ warnings.push('BUNNYFY_ENABLED está ativo, mas nenhuma capacidade BunnyFy saiu de off.');
+ } else {
+ if (!bunnyFyBaseUrl) failures.push('BunnyFy está ativa em alguma capacidade, mas BUNNYFY_BASE_URL está vazio.');
+ if (!bunnyFyToken) failures.push('BunnyFy está ativa em alguma capacidade, mas BUNNYFY_API_TOKEN está vazio.');
+ if (bunnyFyBaseUrl && bunnyFyToken) {
+ console.log(`✅ BunnyFy — configurada para ${activeModes.length} capacidade(s), sem exibir credencial`);
+ }
+ }
+
+ const aiMode = bunnyFyEnabled
+ ? String(process.env.BUNNYFY_AI_MODE || 'off').trim().toLowerCase()
+ : 'off';
+ const nvidiaConfigured = isPresent(process.env.NVIDIA_API_KEY);
+
+ if (aiMode === 'exclusive') {
+ console.log('ℹ️ IA — exclusiva pela BunnyFy; chave NVIDIA direta não é necessária no bot');
+ } else if (nvidiaConfigured) {
+ console.log(`✅ NVIDIA direta — ${maskedState(process.env.NVIDIA_API_KEY)}`);
+ } else if (aiMode === 'primary') {
+ warnings.push('IA está em primary, mas NVIDIA_API_KEY não foi configurada; o fallback direto ficará indisponível.');
+ } else {
+ console.log('ℹ️ NVIDIA direta — não configurada; recurso opcional');
+ }
+
+ const vexKey = isPresent(process.env.VEX_API_KEY);
+ const vexSite = isPresent(process.env.VEX_SITE);
+ if (vexKey && vexSite) console.log('✅ VEX legado — configurado');
+ else if (vexKey || vexSite) warnings.push('VEX está parcialmente configurada; preencha VEX_API_KEY e VEX_SITE ou deixe ambos vazios.');
+ else console.log('ℹ️ VEX legado — não configurado');
+
+ const uploadToken = isPresent(process.env.UPLOAD_GITHUB_TOKEN);
+ const uploadRepo = isPresent(process.env.UPLOAD_GITHUB_REPO);
+ if (uploadToken && uploadRepo) console.log('✅ Upload GitHub legado — configurado');
+ else if (uploadToken || uploadRepo) warnings.push('Upload GitHub legado está parcialmente configurado; token e repositório precisam existir juntos.');
+ else console.log('ℹ️ Upload GitHub legado — não configurado');
+}
+
console.log('\n⛩️ Verificação do ambiente\n');
const nodeVersion = versionTuple(process.versions.node);
if (atLeast(nodeVersion, MIN_NODE)) console.log(`✅ Node.js — v${process.versions.node}`);
-else failures.push(`Node.js ${MIN_NODE.join('.')} ou superior é necessário; atual: ${process.versions.node}`);
+else failures.push(`Node.js ${MIN_NODE.join('.')} ou superior é necessário; atual: v${process.versions.node}`);
probe('npm', process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version']);
probe('Git', 'git', ['--version']);
@@ -46,23 +163,46 @@ const isTermux = Boolean(process.env.TERMUX_VERSION) || fs.existsSync('/data/dat
const platformName = isTermux ? 'Termux/Android' : `${process.platform}/${process.arch}`;
console.log(`✅ Plataforma detectada — ${platformName}`);
-if (isTermux && !probe('termux-wake-lock', 'termux-wake-lock', [], false)) {
- warnings.push('Use pkg install termux-api e o aplicativo Termux:API para manter o aparelho acordado.');
+if (isTermux) {
+ const prefix = process.env.PREFIX || '/data/data/com.termux/files/usr';
+ const wakeLockPath = path.join(prefix, 'bin', 'termux-wake-lock');
+ if (fs.existsSync(wakeLockPath)) console.log('✅ termux-wake-lock — disponível');
+ else warnings.push('termux-wake-lock não foi encontrado. Atualize termux-tools com: pkg install termux-tools. Não é necessário instalar Termux:API para esse comando.');
+}
+
+const envPath = path.join(ROOT, '.env.local');
+if (fs.existsSync(envPath)) {
+ try {
+ loadLocalEnv();
+ console.log('✅ .env.local — carregado sem exibir valores');
+ } catch (error) {
+ failures.push(`.env.local não pôde ser carregado: ${error.message}`);
+ }
+} else {
+ warnings.push('`.env.local` ainda não existe. Copie `.env.example` para `.env.local` ou execute o instalador oficial da plataforma.');
}
const configPath = path.join(ROOT, 'dados', 'src', 'config.json');
const modulesPath = path.join(ROOT, 'node_modules');
-if (fs.existsSync(configPath)) console.log('✅ Configuração local encontrada');
-else warnings.push('Configuração local ainda não criada; execute npm run setup.');
+const localConfig = readLocalConfig(configPath);
+if (localConfig) {
+ console.log('✅ Configuração local encontrada');
+ inspectInstanceConfig(localConfig);
+} else if (!fs.existsSync(configPath)) {
+ warnings.push('Configuração local ainda não criada; execute npm run setup.');
+}
+
if (fs.existsSync(modulesPath)) console.log('✅ Dependências locais encontradas');
else warnings.push('Dependências ainda não instaladas; execute npm ci.');
+inspectOptionalIntegrations();
+
for (const warning of warnings) console.log(`⚠️ ${warning}`);
for (const failure of failures) console.log(`❌ ${failure}`);
if (failures.length) {
- console.log('\n🚫 O ambiente ainda não está pronto. Corrija os itens acima e repita npm run preflight.\n');
+ console.log('\n🚫 O ambiente ainda não está pronto. Corrija os itens obrigatórios acima e repita npm run preflight.\n');
process.exit(1);
}
-console.log('\n🛡️ Ambiente pronto. Siga para a configuração.\n');
+console.log('\n🛡️ Ambiente base pronto. Integrações marcadas como opcionais podem ser configuradas depois.\n');
diff --git a/scripts/validate-instance-config-contract.mjs b/scripts/validate-instance-config-contract.mjs
new file mode 100644
index 0000000..461b0da
--- /dev/null
+++ b/scripts/validate-instance-config-contract.mjs
@@ -0,0 +1,239 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import path from 'node:path';
+
+const ROOT = path.resolve(import.meta.dirname, '..');
+const failures = [];
+const notes = [];
+
+// Somente código de runtime. Scripts de CI/manutenção podem usar variáveis
+// operacionais que não pertencem ao contrato de configuração do usuário.
+const RUNTIME_ROOTS = [
+ path.join(ROOT, 'dados', 'src'),
+ path.join(ROOT, 'src')
+];
+
+const SKIP_DIRS = new Set([
+ '.git',
+ 'node_modules',
+ 'dist',
+ 'dist-vnext',
+ 'coverage',
+ 'dados/database'
+]);
+
+const USER_CONFIG_KEY = /^(?:BUNNYFY_|NVIDIA_|VEX_|UPLOAD_GITHUB_|SHOGUN_|BOT_NAME$|DEFAULT_PERSONA$)/;
+const SECRET_LIKE = /(?:KEY|TOKEN|SECRET|PASSWORD|COOKIE)/;
+const ALLOWED_MODES = new Set(['off', 'primary', 'exclusive']);
+
+function read(file) {
+ return fs.readFileSync(path.join(ROOT, file), 'utf8');
+}
+
+function walk(dir, output = []) {
+ if (!fs.existsSync(dir)) return output;
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ const relative = path.relative(ROOT, full).replaceAll(path.sep, '/');
+ if (entry.isDirectory()) {
+ if (SKIP_DIRS.has(entry.name) || SKIP_DIRS.has(relative)) continue;
+ walk(full, output);
+ continue;
+ }
+ if (/\.(?:[cm]?js|ts)$/.test(entry.name)) output.push(full);
+ }
+ return output;
+}
+
+function parseEnvExample() {
+ const env = new Map();
+ for (const rawLine of read('.env.example').split(/\r?\n/)) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith('#')) continue;
+ const match = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
+ if (!match) {
+ failures.push(`Linha inválida em .env.example: ${rawLine}`);
+ continue;
+ }
+ if (env.has(match[1])) failures.push(`Chave duplicada em .env.example: ${match[1]}`);
+ env.set(match[1], match[2]);
+ }
+ return env;
+}
+
+function collectRuntimeKeys() {
+ const keys = new Map();
+ const patterns = [
+ /process\.env\.([A-Z][A-Z0-9_]*)/g,
+ /process\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/g,
+ /\benv\.([A-Z][A-Z0-9_]*)/g,
+ // Modos são frequentemente passados dinamicamente como string para
+ // resolveCapabilityMode(name, env), portanto não aparecem como env.KEY.
+ // Restringir a *_MODE evita confundir códigos de erro BUNNYFY_*.
+ /['"](BUNNYFY_[A-Z0-9_]+_MODE)['"]/g
+ ];
+ const files = RUNTIME_ROOTS.flatMap(sourceRoot => walk(sourceRoot, []));
+
+ for (const file of files) {
+ const relative = path.relative(ROOT, file).replaceAll(path.sep, '/');
+ const content = fs.readFileSync(file, 'utf8');
+ for (const pattern of patterns) {
+ pattern.lastIndex = 0;
+ let match;
+ while ((match = pattern.exec(content)) !== null) {
+ const key = match[1];
+ if (!USER_CONFIG_KEY.test(key)) continue;
+ if (!keys.has(key)) keys.set(key, new Set());
+ keys.get(key).add(relative);
+ }
+ }
+ }
+ return keys;
+}
+
+function requireEnvKey(env, key) {
+ if (!env.has(key)) failures.push(`.env.example não documenta ${key}.`);
+}
+
+function expectValue(env, key, expected) {
+ requireEnvKey(env, key);
+ if (env.has(key) && env.get(key) !== expected) {
+ failures.push(`${key} deve iniciar como ${JSON.stringify(expected)} no exemplo público.`);
+ }
+}
+
+function validateSecretsAreBlank(env) {
+ for (const [key, value] of env.entries()) {
+ if (!SECRET_LIKE.test(key)) continue;
+ if (value.trim()) failures.push(`${key} parece segredo e deve ficar vazio em .env.example.`);
+ }
+}
+
+function validateModes(env) {
+ for (const [key, value] of env.entries()) {
+ if (!key.startsWith('BUNNYFY_') || !key.endsWith('_MODE')) continue;
+ if (!ALLOWED_MODES.has(value)) {
+ failures.push(`${key} deve usar um modo seguro conhecido no exemplo: off, primary ou exclusive.`);
+ }
+ if (value !== 'off') failures.push(`${key} deve começar em off na distribuição pública.`);
+ }
+}
+
+function validateInstaller(file) {
+ const content = read(file);
+ if (!content.includes('.env.example') || !content.includes('.env.local')) {
+ failures.push(`${file} não garante/explica o bootstrap de .env.local.`);
+ }
+ if (!content.includes('npm run setup')) failures.push(`${file} não executa npm run setup.`);
+}
+
+function validateGuide(file) {
+ const content = read(file);
+ if (!content.includes('configuracao-da-instancia.md')) {
+ failures.push(`${file} não aponta para o guia canônico de configuração.`);
+ }
+ if (!/dono principal/i.test(content)) failures.push(`${file} não explica o dono principal da instância.`);
+ if (!/nenhuma chave|sem chave de API|não precisa de (?:uma )?chave/i.test(content)) {
+ failures.push(`${file} não deixa claro que o núcleo inicia sem chave de API.`);
+ }
+}
+
+const env = parseEnvExample();
+const runtimeKeys = collectRuntimeKeys();
+
+for (const [key, files] of runtimeKeys.entries()) {
+ if (!env.has(key)) {
+ failures.push(`Configuração usada pelo runtime não documentada: ${key} (${[...files].slice(0, 4).join(', ')}).`);
+ }
+}
+
+for (const key of [
+ 'BOT_NAME',
+ 'DEFAULT_PERSONA',
+ 'SHOGUN_LOW_MEMORY',
+ 'BUNNYFY_ENABLED',
+ 'BUNNYFY_BASE_URL',
+ 'BUNNYFY_API_TOKEN',
+ 'BUNNYFY_AI_MODE',
+ 'BUNNYFY_YOUTUBE_MODE',
+ 'BUNNYFY_TRANSCRIPTION_MODE',
+ 'BUNNYFY_FACEBOOK_MODE',
+ 'BUNNYFY_PINTEREST_MODE',
+ 'BUNNYFY_TIKTOK_MODE',
+ 'BUNNYFY_KWAI_MODE',
+ 'BUNNYFY_IMAGES_MODE',
+ 'BUNNYFY_STICKERS_MODE',
+ 'BUNNYFY_CANVAS_MODE',
+ 'BUNNYFY_LOGOS_MODE',
+ 'BUNNYFY_GAMES_MODE',
+ 'BUNNYFY_IMAGE_GEN_MODE',
+ 'BUNNYFY_TAVERN_RENDER_MODE',
+ 'BUNNYFY_NEXO_RENDER_MODE',
+ 'NVIDIA_API_KEY',
+ 'VEX_API_KEY',
+ 'VEX_SITE',
+ 'UPLOAD_GITHUB_TOKEN',
+ 'UPLOAD_GITHUB_REPO'
+]) requireEnvKey(env, key);
+
+expectValue(env, 'BUNNYFY_ENABLED', 'false');
+validateSecretsAreBlank(env);
+validateModes(env);
+
+for (const file of [
+ 'scripts/install-linux.sh',
+ 'scripts/install-macos.sh',
+ 'scripts/install-termux.sh',
+ 'scripts/install-windows.ps1'
+]) validateInstaller(file);
+
+for (const file of [
+ 'README.md',
+ 'docs/primeiros-passos.md',
+ 'docs/instalacao/linux.md',
+ 'docs/instalacao/macos.md',
+ 'docs/instalacao/termux.md',
+ 'docs/instalacao/windows.md'
+]) validateGuide(file);
+
+const configExample = JSON.parse(read('dados/src/config.example.json'));
+for (const key of ['nomedono', 'numerodono', 'nomebot', 'prefixo']) {
+ if (!Object.hasOwn(configExample, key)) failures.push(`config.example.json não contém ${key}.`);
+}
+if (configExample.numerodono !== '55DDDNUMERO') {
+ failures.push('config.example.json deve manter número neutro, nunca um número real de dono.');
+}
+
+const preflight = read('scripts/preflight-platform.mjs');
+for (const key of [
+ 'BUNNYFY_ENABLED',
+ 'BUNNYFY_BASE_URL',
+ 'BUNNYFY_API_TOKEN',
+ 'BUNNYFY_TRANSCRIPTION_MODE',
+ 'BUNNYFY_FACEBOOK_MODE',
+ 'BUNNYFY_PINTEREST_MODE',
+ 'BUNNYFY_TIKTOK_MODE',
+ 'BUNNYFY_KWAI_MODE',
+ 'NVIDIA_API_KEY',
+ 'VEX_API_KEY',
+ 'VEX_SITE'
+]) {
+ if (!preflight.includes(key)) failures.push(`preflight não audita ${key}.`);
+}
+if (!preflight.includes('Dono principal da instância')) {
+ failures.push('preflight não valida a identidade do dono principal da instância.');
+}
+
+notes.push(`${runtimeKeys.size} variáveis/modos de configuração da instância encontrados no runtime.`);
+notes.push(`${env.size} entradas documentadas em .env.example.`);
+
+for (const note of notes) console.log(`ℹ️ ${note}`);
+for (const failure of failures) console.error(`❌ ${failure}`);
+
+if (failures.length) {
+ console.error(`\nContrato de configuração reprovado: ${failures.length} problema(s).\n`);
+ process.exit(1);
+}
+
+console.log('\n✅ Contrato de configuração da instância aprovado.\n');