# Guia de contribuição de plugins O sistema de plugins do Voyager dá prioridade a plugins declarativos: `plugin.json` descreve os metadados e operações DOM, enquanto o CSS descreve os estilos. O plugin não executa JavaScript remoto; o motor de plugins integrado do Voyager interpreta o manifest e os estilos. Isto torna os plugins mais fáceis de rever e manter. Se quiser contribuir com um plugin, comece por este caminho. ## Só para você: plugins locais Um plugin de que você só precisa no seu próprio navegador não passa por este processo. Crie-o como plugin local e importe-o no popup: sem Issue, sem PR, sem clonar o repositório. [Escreva seu próprio plugin](./write-a-plugin.md) explica as três formas de criar um, incluindo o verificador `create-voyager-plugin` e a skill para agentes de código. ## Caminho recomendado 1. Confirme primeiro que a ideia é adequada para plugin: largura de leitura, correções de layout, ajustes de tema, ocultar ou marcar elementos da página e adaptações simples de sites são bons candidatos. 2. Abra primeiro uma Issue no repositório principal do Voyager. Explique o problema, o site alvo e a diferença face aos plugins existentes; aguarde a aprovação explícita de um mantenedor antes de programar ou abrir uma PR. 3. Use `plugin.json` para metadados, sites correspondentes, definições e contribuições. 4. Coloque os estilos em `style.css` no mesmo diretório e referencie-o em `contributes.styles`. 5. Teste localmente e inclua páginas de teste, capturas de ecrã ou uma gravação curta na PR. Os mantenedores decidirão se está pronto para o catalog oficial. ## Estrutura de diretórios Os plugins oficiais incluídos ficam em `src/features/plugins/catalog/`, com um diretório por plataforma: ``` src/features/plugins/catalog/ marketplace.json índice lido pela loja da documentação sites//site.json o adaptador de site, como dados sites//plugins//plugin.json um plugin declarativo sites//plugins//style.css os seus estilos sites//plugins//README.md o que corrige e porquê ``` A descoberta é automática: `catalog/sites/index.ts` encontra cada `site.json` e cada `plugin.json` com `import.meta.glob`, por isso acrescentar um site ou um plugin é acrescentar ficheiros. Não há tabela de mapeamento para editar. `marketplace.json` não é essa tabela: é apenas o índice lido pela loja de plugins da documentação. Um teste mantém-no sincronizado com a descoberta, portanto um plugin novo também precisa de uma entrada aí. O `source` é o caminho relativo ao catalog, por exemplo `sites/deepseek/plugins/reading-width/plugin.json`. Os plugins em `sites/gemini/` e `sites/aistudio/` são a exceção: vão dentro do Voyager e ficam fora do `marketplace.json`. Os `matches` de um plugin têm de ficar dentro dos `matches` do seu site. Um plugin que saia desse âmbito faz a build falhar. ## Chaves de seletor semântico `site.json` associa um vocabulário fixo de chaves semânticas a seletores CSS próprios do site. O vocabulário está definido em `src/features/plugins/sites/semanticKeys.ts`; um `site.json` só pode usar estas chaves e qualquer chave desconhecida é rejeitada. - `userTurn`: o contentor de uma mensagem do utilizador. - `assistantTurn`: o contentor de uma mensagem do assistente. - `thinkingBlock`: a secção de raciocínio dentro de uma resposta. - `codeBlock`: um bloco de código renderizado. - `composer`: o campo onde se escreve o prompt. - `sidebar`: a lista de conversas ou a barra de navegação. - `sidePanel`: um painel secundário, como artifacts ou canvas. - `headerActions`: o grupo de ações no canto superior direito da conversa. - `scrollContainer`: o elemento que faz scroll da conversa. - `composerRoot`: o contentor que agrupa o campo do prompt e os seus botões de ação. - `sendButton`: o botão que envia o prompt; alguns sites só o mostram quando o campo tem texto. Um site declara apenas as chaves que consegue mesmo fornecer. Os plugins referenciam uma chave em vez de um seletor, escrevendo `{ "kind": "semantic", "key": "userTurn" }` como `target` de uma operação DOM, por isso uma remodelação do site resolve-se num único `site.json`. `conversationIdPattern` é um campo separado do `site.json`, não um seletor: uma expressão regular sobre o caminho do URL cujo primeiro grupo de captura é o id da conversa, por exemplo `^/chat/([^/?#]+)`. ## Adicionando uma nova plataforma Adicionar um novo site de chat tem duas camadas separadas: 1. O **adaptador do site**, `catalog/sites//site.json`, ensina o Voyager a reconhecer o site e a encontrar as mensagens do usuário, as respostas do assistente, a entrada de prompt, a barra lateral e os marcadores de tema. 2. Um **plugin** em `catalog/sites//plugins//` resolve um problema específico do usuário nesse site, como a largura de leitura ou a correção de uma renderização. Um `site.json` contém `id` (igual ao nome do diretório), `label`, `matches`, `selectors`, `theme`, `brandColor`, `capabilities` e, opcionalmente, `conversationIdPattern` e `turnKeyAttributes`. O `turnKeyAttributes` indica os atributos que mantêm a identidade de uma mensagem do usuário quando a página a renderiza de novo; o rastreamento de envio (usado pela Atividade das pastas e pelos carimbos de data/hora da linha do tempo) só roda em um site que o defina, junto com `composer`, `userTurn` e um `conversationIdPattern`. `validateSiteAdapterData`, em `src/features/plugins/sites/siteAdapterData.ts`, faz essa validação, e `bun run catalog:build` valida todos os sites e plugins antes da publicação. `site.json` é o próprio adaptador de site escrito como dados. Hoje as plataformas são ChatGPT, Claude e DeepSeek. O Gemini e o AI Studio são superfícies nativas do Voyager com o seu próprio `site.json` (`catalog/sites/gemini/` e `catalog/sites/aistudio/`), mas este vai dentro do pacote e nunca é publicado no catálogo remoto; `sites/adapters/gemini.ts`, `aistudio.ts`, `claude.ts`, `chatgpt.ts` e `deepseek.ts` são invólucros de uma linha sobre o respetivo `site.json`: edite o JSON, não o TypeScript. O catálogo publicado por host transporta também os dados do site, por isso uma correção de seletores chega aos utilizadores sem uma nova versão da extensão. Um site completamente novo ainda precisa de uma nova versão, porque também depende de permissão de host e do registro do content script, que vêm dentro do pacote. Para uma nova plataforma, como o DeepSeek, mantenha o trabalho do adaptador focado no contrato da plataforma. Coloque o `site.json`, os testes obrigatórios de seletores e a documentação mínima necessária para explicar o suporte em uma única mudança completa. O registro de sites pega o novo adaptador a partir do próprio arquivo, então não há lista de registro para editar. Não adicione um novo site para cada plugin. Depois que o adaptador for aceito, os plugins seguintes podem ser enviados como mudanças separadas e focadas. Se várias mudanças dependem umas das outras, uma cadeia de PRs empilhados é útil: cada branch posterior é baseada na anterior, e cada PR mostra apenas a sua própria funcionalidade incremental. A cadeia ainda deve seguir a mesma regra de escopo: um objetivo claro para o usuário por PR, com seus testes obrigatórios e a limpeza necessária. Suporte a um site não exige integração com API de modelo. Não adicione content scripts estáticos nem permissões de host obrigatórias para uma nova plataforma; use o fluxo existente de permissão opcional e registro dinâmico para sites que têm apenas plugins. O caminho de permissão opcional e de registro dinâmico depende da versão do navegador. Chrome e Edge suportam o fluxo usado pela extensão. O Firefox exige a versão 128 ou mais recente para permissões de host opcionais, e o Safari exige a versão 16.4 ou mais recente para o registro dinâmico de content scripts. Em versões mais antigas dos navegadores compatíveis, o popup pode recusar corretamente ativar um plugin para um site personalizado, porque as APIs necessárias não conseguem manter uma permissão persistente; não contorne isso adicionando uma permissão de host estática ampla sem aprovação do mantenedor. ## Âmbito do plugin O âmbito deve seguir o problema do utilizador, não uma divisão mecânica por plataforma. Um plugin do catálogo vive na pasta de um único site e todos os seus `matches` têm de estar cobertos pelo `site.json` dessa pasta (caso contrário, `bun run plugin:check` falha), por isso um mesmo plugin não pode cobrir Claude e ChatGPT. Quando uma funcionalidade serve várias plataformas, como largura de leitura, navegação entre páginas ou layout de blocos de código, publique um plugin por pasta de site (por exemplo `sites/claude/plugins/reading-width` e `sites/chatgpt/plugins/reading-width`) e mantenha nomes, definições e descrições consistentes. Se cada plataforma exigir definições, lógica DOM ou textos muito diferentes, plugins separados serão mais claros. Não force funções sem relação num único plugin só para "cobrir tudo"; um plugin deve resolver um problema claro. Regra rápida: - Mesmo objetivo do utilizador, mesmas definições, só mudam os seletores: um plugin por pasta de site, com nomes e definições consistentes. - Mesmo tema, mas experiência muito diferente por plataforma: pode separar, mantendo nomes e descrições relacionados. - Objetivos diferentes: não junte. ## Evitar plugins duplicados Antes de submeter, verifique o marketplace e os plugins oficiais existentes. Se já houver um bom plugin, melhore-o em vez de criar outro semelhante. Um duplicado só deve ser aceite quando traz uma melhoria clara, por exemplo: - Cobre uma plataforma importante que o plugin original não suporta. - Corrige um problema de compatibilidade que o original não consegue resolver. - Tem melhor desempenho, acessibilidade ou manutenção de forma clara. - Oferece uma experiência de utilizador diferente e útil, não apenas outro nome ou pequenas alterações de estilo. Assim o marketplace fica mais limpo e os utilizadores escolhem melhor. ## Exemplo mínimo Não comece por um diretório vazio: `bun run plugin:new --site ` cria `catalog/sites//plugins//` com um `plugin.json`, um `style.css` e um `README.md`, e acrescenta a entrada correspondente ao `marketplace.json`. O que gera é um ponto de partida, não uma aprovação; o manifest que vai preencher tem esta forma: ```json { "$schema": "https://voyager.nagi.fun/plugin.schema.json", "id": "your-name.example-plugin", "name": "Example Plugin", "version": "1.0.0", "description": "A short description of what this plugin improves.", "author": "your-name", "category": "readability", "license": "MIT", "engine": ">=1.0.0", "tier": "declarative", "matches": ["https://claude.ai/*"], "contributes": { "styles": [{ "file": "style.css" }], "domOps": [ { "op": "addClass", "target": "body", "className": "gv-plugin-example" } ] } } ``` A linha `$schema` é opcional e serve apenas ao seu editor: aponta para o JSON Schema do manifest, para que este seja validado e completado enquanto escreve. O `site.json` tem o seu em `https://voyager.nagi.fun/site.schema.json`. `style.css` pode ser escrito como CSS normal, mas recomenda-se que todos os estilos fiquem dentro da sua própria classe `gv-plugin-*`: ```css .gv-plugin-example .some-target { max-width: 880px; } ``` ## Notas do manifest - Use um prefixo de autor ou estilo de domínio inverso para `id`, como `your-name.reading-width`, para evitar conflitos. - Mantenha `matches` restrito aos sites onde o plugin realmente precisa de funcionar. - Um plugin só pode listar vários hosts em `matches` se o `site.json` do seu site os cobrir todos (por exemplo chatgpt.com e chat.openai.com); para cobrir outro site, adicione um plugin na pasta desse site. - Valores recomendados para `category`: `render-fix`, `theme`, `layout`, `readability`, `productivity`, `integration` ou `other`. - Indique em `engine` a versão do motor de plugins necessária. Os plugins oficiais podem servir de exemplo. - Adicione `i18n` para chinês, inglês e outras línguas comuns quando possível. ## Limites de CSS e recursos Plugins declarativos são validados como entrada não confiável, por isso mantenha os recursos autocontidos: - Não use `@import`. - Não referencie imagens remotas, fontes externas ou CSS remoto. - Pode usar CSS normal, propriedades personalizadas e substituições de valores de definições fornecidas pelo Voyager. - Use o prefixo `gv-plugin-` nas classes para evitar poluir o site anfitrião ou o próprio Voyager. Se o plugin precisar de definições, comece preferencialmente por valores numéricos. Por exemplo, um plugin de largura de leitura pode escrever o valor numa variável CSS e consumi-la no CSS. ## Limites das operações DOM Plugins declarativos suportam atualmente: - `addClass`: adiciona uma classe aos elementos alvo. - `setAttribute`: define um atributo. - `setStyle`: define estilos inline ou variáveis CSS. Termine um valor com `!important` para o aplicar com essa prioridade; versões mais antigas do Voyager descartam esse valor, por isso defina `engine` como `>=1.9.0` se o plugin depender disso. Atalhos como `margin` são acompanhados por propriedade longa, por isso desativar um plugin nunca apaga o `margin-top` de outro. - `hide`: oculta elementos alvo. O alvo pode ser um seletor CSS ou uma das chaves semânticas listadas acima, escrita como `{ "kind": "semantic", "key": "userTurn" }`. As chaves semânticas costumam ser mais estáveis, mas o adaptador do site tem de declarar essa chave. As operações declarativas devem ser reversíveis e seguras para executar repetidamente. Não dependa de um estado único da página nem assuma que o DOM nunca muda. ### Primitivas Alguns comportamentos não se conseguem descrever com CSS e uma alteração reversível do DOM. Uma primitiva é código próprio que segue dentro do Voyager e que um manifest pode chamar pelo nome através da operação `native`: ```json { "engine": ">=1.3.0", "requires": { "handlers": ["formulaCopy"] }, "contributes": { "domOps": [{ "op": "native", "handler": "formulaCopy", "params": {} }] } } ``` O manifest escolhe uma primitiva e configura-a; nunca fornece lógica, e a própria primitiva valida `params` antes de correr. Aplicam-se duas regras a qualquer plugin que use uma: - Cada site coberto pelo plugin tem de declarar as chaves semânticas que a primitiva lê (`mermaid`, por exemplo, lê `assistantTurn` e `codeBlock`). `requires.handlers` é opcional: a operação `native` já implica a sua primitiva. - `engine` tem de ser pelo menos a versão do motor que a lançou pela primeira vez. A `formulaCopy` chegou no motor 1.3.0, por isso um plugin que a use declara `">=1.3.0"`. Um intervalo inferior faz falhar o `bun run catalog:build`, porque uma versão antiga do Voyager reportaria então um handler em falta em vez de pedir ao utilizador que atualize. As primitivas só crescem: um parâmetro novo é sempre opcional e uma alteração incompatível sai com um nome novo. Um plugin que mude de comportamento pode ainda trazer um `changelog` de uma linha, que o popup mostra ao lado da versão. Traduza-o em `i18n..changelog`, junto de `name` e `description`. ## Quando não usar um plugin normal Se a funcionalidade precisar de executar JavaScript, intercetar pedidos, ler ou escrever dados internos do Voyager, ou depender de lógica complexa em tempo de execução, não é adequada para um plugin declarativo normal. Abra primeiro uma Issue e explique a necessidade. Se exigir mesmo uma capacidade integrada, podemos considerar implementá-la no repositório do Voyager como plugin builtin/native, como o Formula Copy. ## Antes de abrir uma PR Execute `bun run plugin:check ` e cole a saída na PR. Comunica todos os problemas de uma vez: o manifest e o CSS, se `matches` sai do âmbito do site, as primitivas, as chaves semânticas, os dez idiomas e o README; a CI corre a mesma verificação sobre todos os plugins incluídos. - O plugin está desativado por padrão e o utilizador ativa-o manualmente. - Verificou que não existe um plugin quase igual; se existir, melhorou o plugin existente primeiro. - Testou o site alvo em tema claro e escuro. - `matches` não cobre sites sem relação. - Não há referências a recursos remotos. - O diretório do plugin contém `plugin.json`, os ficheiros CSS necessários e um README curto. - Sendo um plugin oficial, o diretório está em `catalog/sites//plugins//`, os `matches` ficam dentro dos do site e `catalog/marketplace.json` lista-o (exceto os plugins do Gemini e do AI Studio, que vão dentro do Voyager). - A PR descreve páginas de teste, capturas ou gravações, e as áreas de página afetadas. Mantenha simples, focado e reversível. Um plugin que resolve um problema claro é muito mais fácil de fundir e manter.