Consulte as vendas do contato no C-Plus 5 dentro do atendimento. Sem build ou dependências no navegador.
Protocolo · Publicação · MIT
Este repositório contém um plugin estático. O botão aparece na barra lateral direita do atendimento. Ao selecionar um atendimento, o painel encontra a pessoa no Novus CRM e o cliente no C-Plus 5. Depois mostra as vendas com busca e paginação.
O mesmo código funciona com Servidor próprio ou Hospedado no Novus. A diferença é onde os arquivos ficam e como você publica uma versão.
| Arquivo | Para que serve |
|---|---|
index.html |
Painel exibido no iframe |
plugin.js |
Protocolo postMessage e lógica de vendas |
novus-plugin.json |
Versão, entrada e permissões do pacote hospedado |
README.md |
Configuração e publicação deste exemplo |
| Servidor próprio | Hospedado no Novus | |
|---|---|---|
| Cadastro | Título e URL da página | Título e domínio do plugin |
| Arquivos | No seu servidor, com HTTPS | Num pacote .zip enviado ao Novus CRM |
| Permissões | Informadas no cadastro | Lidas de novus-plugin.json |
| Versões | Você gerencia no seu servidor | Envio, pré-visualização, publicação e reversão na tela Versões |
O registro em plugin.js decide onde o botão aparece. O cadastro escolhe a hospedagem, o título e quem pode usar o plugin.
Hospede index.html e plugin.js em HTTPS. A página precisa aceitar iframe. Não envie X-Frame-Options ou Content-Security-Policy: frame-ancestors que impeçam a incorporação pelo Novus CRM.
Em Opções > Plugins, escolha Servidor próprio e informe o título e a URL completa de index.html. Declare os dois hosts, as duas variáveis e widgetbar conforme o manifesto abaixo. O arquivo novus-plugin.json continua como referência, mas o Novus CRM não o lê da sua hospedagem.
-
Em Opções > Plugins, clique em Incluir, escolha Hospedado no Novus, informe o título e o domínio desejado. Salve. A URL será
https://{dominio}.novusplugin.com/. -
Confira
novus-plugin.json. Antes de cada novo envio, aumenteversao. Para este exemplo, o primeiro pacote usa1.0.0. -
Gere o pacote somente com os três arquivos do site:
zip -j plugin-1.0.0.zip novus-plugin.json index.html plugin.js unzip -Z -1 plugin-1.0.0.zip
A segunda linha deve listar
novus-plugin.json,index.htmleplugin.js, sem diretório acima deles. Não compacte a raiz do repositório.LICENSE,.git/e outros arquivos de desenvolvimento não pertencem ao pacote. -
Abra Versões no plugin e envie o
.zip. O envio cria uma versão, mas não muda a versão ativa. Confira os avisos e use Pré-visualizar para conferir a página no seu navegador. As chamadas ao proxy seguem as permissões da versão ativa. -
Clique em Publicar, revise as permissões mostradas na tela e confirme.
-
Recarregue o atendimento. O plugin publicado aparece no painel. Se precisar voltar, use Voltar para esta versão numa versão anterior.
A publicação chega ao domínio do plugin em até um minuto. A URL ativa verifica atualizações a cada carregamento. Um iframe já aberto não troca de versão sozinho. Cada versão também tem URL imutável: https://{dominio}--v{numero}.novusplugin.com/.
O domínio tem de 3 a 40 caracteres, com letras minúsculas, números e hífens. Começa e termina com letra ou número, não aceita --, é único entre contas e fica fixo depois da primeira publicação. Nomes como www, api, app, admin, docs e suporte são reservados.
A versão do manifesto segue SemVer, como 1.0.0 ou 1.1.0, e não pode ser usada em dois pacotes diferentes do mesmo plugin. Reenviar o mesmo pacote devolve a versão já existente. O Novus CRM guarda as 20 versões mais recentes, a ativa e as instaladas por outras contas.
O arquivo novus-plugin.json faz parte do pacote. Suas permissões correspondem ao uso real em plugin.js:
{
"nome": "vendas-cplus5",
"versao": "1.0.0",
"entrada": "index.html",
"spa": false,
"permissoes": {
"hosts": ["publica", "cplus5"],
"dominiosExternos": [],
"segredos": ["cplus.chaveapi", "cplus.ambiente"],
"superficies": ["widgetbar"]
}
}| Campo | Regra neste exemplo |
|---|---|
nome |
Identifica o plugin no manifesto |
versao |
SemVer. Aumente antes de enviar conteúdo alterado |
entrada |
Página aberta na raiz. O padrão é index.html |
spa |
false porque o painel não usa rotas do navegador |
permissoes.hosts |
publica consulta a API do Novus CRM. cplus5 consulta o ERP |
permissoes.dominiosExternos |
Vazio porque o código não usa o host externo |
permissoes.segredos |
Nomes citados em secretRefs, sem os valores |
permissoes.superficies |
widgetbar é a única superfície registrada |
Os hosts reconhecidos pelo manifesto são publica, cplus, cplus5, vcitech e externo. Para usar externo, declare também os domínios em dominiosExternos. Informe só o domínio, sem protocolo, porta ou caminho. *.exemplo.com cobre subdomínios, mas não exemplo.com.
No plugin hospedado, o proxy usa hosts e segredos da versão ativa. Uma versão apenas enviada não muda as permissões em uso.
As superfícies definem onde os itens do plugin aparecem no Novus CRM. Registre os itens em initialize e declare cada superfície usada em permissoes.superficies. No servidor próprio, faça essa declaração no cadastro do plugin. No plugin hospedado, um item sem a superfície declarada é ignorado, com aviso no console.
Registro em initialize |
Valor em superficies |
Onde aparece e o que faz |
|---|---|---|
widgetbar |
widgetbar |
Ícone na barra lateral direita do atendimento. Sem callback, abre o painel do plugin. Com callback, executa a função ao clicar, sem abrir o painel automaticamente. |
navbar |
navbar |
Item no menu principal, na seção Integrações. Ao clicar, abre a página do plugin no CRM e executa o callback, se houver. |
options |
options |
Item em Opções, na área de plugins. Ao clicar, abre a página do plugin dentro das opções e executa o callback, se houver. |
buttons |
buttons:{grupo} |
Ação em um menu ou no cabeçalho de uma tela. O grupo define o local e os dados recebidos pelo callback. |
Em navbar e options, use type: "group" para criar um cabeçalho e type: "item" para criar uma opção clicável. item é o padrão. Defina parentId no item para colocá-lo dentro de um grupo com aquele id. Grupos não executam callback.
Dê um id estável a cada item de navbar e options. Assim, o Novus CRM consegue executar o callback no iframe da página, mesmo quando ela termina de carregar após o clique. Sem id, um callback de navbar roda no iframe em segundo plano.
Cada grupo de buttons tem sua própria permissão. Declare o valor exato, como buttons:ticket-menu, para cada grupo que registrar:
Grupo em buttons |
Onde aparece | O que o callback recebe |
|---|---|---|
lista-contatos |
Menu de ações de cada contato na lista de contatos | { id, nome, numero, ehGrupo } |
atendimento-chat |
Cabeçalho do atendimento de chat aberto. Em telas estreitas, o botão pode ir para o menu ⋮ |
Resumo { id, usuario, contato, canal } |
atendimento-chat-menu |
Menu ⋮ do atendimento de chat |
O mesmo resumo |
atendimento-page |
Menu ⋮ do atendimento de chat |
O mesmo resumo |
ticket-menu |
Menu Opções do ticket, em um ticket já salvo | O ticket aberto, com campos, vínculos, campos personalizados, referências e interacoes |
No botão do Chat, usuario e canal podem ser null ou conter apenas { id }. contato contém { id, numero }. O atendimento chega em id. No evento aoFocarAtendimento, o mesmo identificador chega em atendimentoId, com mais dados do atendimento. Os dois payloads têm formatos diferentes. Os callbacks de widgetbar, navbar e options não recebem payload do clique.
Este exemplo registra somente widgetbar, por isso o manifesto declara apenas widgetbar. Se você adicionar um item em buttons["ticket-menu"], acrescente buttons:ticket-menu às superfícies declaradas. Os eventos registrados em events, como aoAbrirAtendimento, não criam itens na interface e não entram em superficies.
O .zip contém os arquivos estáticos prontos. O Novus CRM não instala dependências nem executa build. novus-plugin.json e o arquivo indicado por entrada precisam existir. Uma única pasta de topo com o manifesto dentro também é aceita.
| Limite | Valor |
|---|---|
| Tamanho compactado | 20 MB |
| Tamanho descompactado | 50 MB |
| Arquivos | 2.000 |
| Profundidade | 12 níveis |
| Compressão por arquivo acima de 1 MB | Até 100 vezes |
Extensões aceitas para páginas e scripts: html, htm, css, js, mjs, json, map, webmanifest, txt e md.
Outros formatos aceitos: svg, png, jpg, jpeg, gif, webp, avif, ico, woff, woff2, ttf, otf e wasm.
O pacote recusa arquivos sem extensão ou de outro tipo. Também recusa links simbólicos, caminho absoluto, .., barra invertida e nomes repetidos que mudam só em maiúsculas e minúsculas. __MACOSX/, .DS_Store e Thumbs.db são ignorados. O Novus CRM lista os problemas do pacote no envio.
O host cplus5 aponta para https://api.cplus.com.br. Este exemplo usa GET v1/Clientes e GET v1/Vendas. O host publica chama a API pública do Novus CRM.
Cadastre estas variáveis da conta antes de usar o plugin:
| Nome | Valor que você cadastra | Uso |
|---|---|---|
chaveApiPublica |
Chave da API pública do Novus CRM | O proxy injeta no host publica. Não entra em secretRefs |
cplus.chaveapi |
X-Chave-Api {sua-chave}, incluindo o prefixo |
Cabeçalho X-Authorization do C-Plus 5 |
cplus.ambiente |
Domínio do ambiente no C-Plus 5 | Cabeçalho X-Ambiente do C-Plus 5 |
Gere a chave do Novus CRM em Configurações > Integrações > Chave de API. Peça a chave e o domínio do ERP ao administrador do C-Plus 5. Depois, abra Configurações > Conta > Minha Conta > Variáveis Globais e cadastre cada nome e valor da tabela.
Nunca coloque uma chave no manifesto, no JavaScript ou no .zip. O nome chaveApiPublica é fixo no proxy. Este exemplo escolheu os nomes cplus.chaveapi e cplus.ambiente. Se você os mudar, atualize plugin.js, novus-plugin.json e o cadastro da conta.
O valor de cplus.chaveapi precisa conter X-Chave-Api antes da chave. O proxy insere o valor inteiro no cabeçalho, sem acrescentar esse prefixo. O endereço do ERP vem do registro de hosts do proxy, não de uma variável chamada cplus.api.
O plugin precisa rodar dentro de um atendimento no Novus CRM. O host publica chama a API pública da conta pelo proxy do Novus CRM. O host cplus5 chama o ERP pelo mesmo proxy. Abrir index.html sozinho ou servir os arquivos em localhost, por si só, não executa essas consultas.
Configure as variáveis acima, cadastre o plugin e publique a versão. Depois, abra um atendimento com contato vinculado a uma pessoa cujo código exista no C-Plus 5. O botão aparece na barra lateral direita. Se ela estiver recolhida ou a tela for estreita, expanda a barra ou use uma tela maior.
Para conferir as vendas, abra a versão publicada dentro do atendimento. A pré-visualização permite conferir a página, mas as chamadas usam as permissões da versão ativa.
O topo de plugin.js implementa o protocolo direto, sem SDK. initialize usa um MessageChannel persistente para receber eventos. Cada apiRequest usa outro canal para receber uma resposta. O initialize é a primeira mensagem enviada ao host.
O código registra um item em widgetbar e escuta abrir, focar e fechar atendimento. O host reenvia o atendimento em foco quando o iframe termina de carregar. Abrir e focar usam a mesma função. Ao receber o mesmo contato de novo, o plugin mantém a página e a busca atuais.
O Novus CRM carrega um iframe em segundo plano para registrar o botão e outro quando abre o painel. Ambos recebem eventos de atendimento. Cada iframe guarda seu próprio estado, então uma consulta pode ocorrer nos dois. Este exemplo só faz consultas GET. Ao adaptar eventos para alterar dados, mova essa ação para um clique do usuário ou garanta que a operação seja idempotente.
Se você trocar de atendimento ou de página da lista antes de uma consulta terminar, o código descarta a resposta anterior. O teste cobre a troca de atendimento durante a busca da pessoa e das vendas.
O enviarComando(command, args) do plugin.js abre um MessageChannel para cada consulta. O host responde com data ou error. A função devolve os dados ou rejeita a Promise. Chame-a depois de initialize:
const usuario = await enviarComando("getInfoUser", null);
// { id, name, email } do usuário logado
const canais = await enviarComando("getInfoChannels", null);
// Lista com canalId, descricao, identificador, type e statusEssas duas consultas são exemplos para adaptar o plugin. O painel de vendas não precisa delas. getInfoChannels também traz number. type é numérico e status vale CONNECTED ou OFFLINE. organizacao e organizacaoId vêm vazios. Para buscar outros dados, use enviarComando("apiRequest", { host: "publica", method: "GET", endpoint: "..." }) com uma rota da API pública. As consultas reais deste plugin estão em plugin.js.
O SDK WlExtension oferece métodos com esses nomes, caso você escolha incluí-lo no seu projeto. WlExtension.identifier traz o id do plugin recebido em ?id=, não o id do usuário. apiRequest() devolve o JSON da API, e openPage() devolve { navigated: boolean }. O guia de comandos mostra todos os métodos.
O atendimento atual chega no callback de aoAbrirAtendimento ou aoFocarAtendimento. O payload inclui atendimentoId, canalId, contato.id, contato.nome e contato.numero. O host reenvia o atendimento em foco após o registro do iframe. Ao receber aoFecharAtendimento, limpe o estado do atendimento. Esse evento não traz payload. Não existe um comando para consultar o atendimento em foco. O usuarioId e o objeto usuario do atendimento indicam o atendente atribuído. Para saber quem está logado, use getInfoUser.
No objeto enviado a initialize, registre os callbacks assim:
events: {
aoFocarAtendimento: registrarEvento("aoFocarAtendimento", (atendimento) => {
const idAtendimento = atendimento.atendimentoId;
const idContato = atendimento.contato.id;
// Use esses IDs para atualizar o painel ou consultar a API pública.
}),
aoFecharAtendimento: registrarEvento("aoFecharAtendimento", () => {
// Limpe o atendimento guardado em memória.
}),
}Um botão recebe no callback o item clicado. Em lista-contatos, recebe o contato. Os grupos atendimento-chat, atendimento-chat-menu e atendimento-page recebem o resumo do atendimento. Em ticket-menu, recebe o ticket, inclusive interacoes. Consulte a tabela de superfícies para escolher onde colocar cada botão.
Use enviarComando para getInfoUser, getInfoChannels, apiRequest, openAlert e openPage. openModal usa um canal próprio para devolver o resultado por callback. closeModal, openWidget e load não devolvem resposta, então não espere por enviarComando nesses casos. O initialize usa o canal persistente dos eventos. Consulte os comandos na documentação para ver os argumentos e o SDK WlExtension.
Ao selecionar um atendimento, o Novus CRM emite aoAbrirAtendimento e aoFocarAtendimento com os mesmos dados. O exemplo evita repetir a consulta para o mesmo contato. Há também eventos para abrir e fechar uma conversa anterior e para entrar no Chat ou na aba Conversas. Os eventos de acesso e fechamento não trazem payload. Veja a lista de eventos.
O contato e o cliente do ERP são cadastros diferentes. O código faz esta sequência:
GET v1/contatodechat/{id}no hostpublicaencontraIdPessoa.GET v1/pessoas/{IdPessoa}no hostpublicaencontraCodigo.GET v1/Clientes?Codigo=...no hostcplus5encontra o ID do cliente no ERP.GET v1/Vendas?IdPessoa=...no hostcplus5traz a página de vendas.
O painel distingue contato sem pessoa, pessoa sem código e cliente ausente no ERP. A busca só com dígitos usa NumeroDaVendaAproximado. Outros termos usam NomeDoProdutoAproximado. A paginação ocorre no servidor do C-Plus 5.
icon_url no registro do widgetbar é um SVG embutido. Se você usar um arquivo próprio, monte a URL absoluta com new URL("icone.svg", location.href).href. O host resolve URLs relativas no domínio do CRM, onde o ícone não existe.
O plugin não recebe o token da sessão nem os valores das variáveis. secretRefs envia o nome da variável e onde ela deve entrar. O servidor resolve o valor e faz a chamada. fetch direto para a API externa pode falhar por CORS e exporia qualquer credencial escrita no navegador.
explicarErro traduz as respostas do proxy. Erro de configuração não mostra Tentar de novo. Falha passageira mostra. O detalhe técnico continua no console com o prefixo [vendas-cplus5].
| O painel mostra | Mensagem do proxy | O que fazer |
|---|---|---|
| Host não disponível | Host not allowed |
Conferir o nome do host usado pelo plugin |
| Plugin não encontrado | Plugin not found |
Conferir o cadastro e a conta |
| Host não declarado | Host not declared |
Declarar o host no manifesto ou no cadastro |
| Domínio não declarado | Domain not declared: ... |
Declarar o domínio se passar a usar externo |
| Variável não declarada | Secret not declared: ... |
Declarar a variável no manifesto ou no cadastro |
| Configuração incompleta | Secret not found: ... |
Cadastrar a variável na conta |
| Chave do Novus CRM ausente | Chave da API pública não configurada... |
Cadastrar chaveApiPublica na conta |
| Consulta não disponível | Endpoint not allowed ou Method not allowed |
Conferir a rota e o método usados |
| Endereço da consulta inválido | Invalid endpoint |
Conferir o endpoint no código publicado |
| Serviço não encontrado | Endpoint not resolvable |
Conferir o domínio do serviço externo |
| Sessão expirada | Unauthorized |
Recarregar o Novus CRM |
| Acesso recusado pelo C-Plus 5 | proxy 401 ou proxy 403 |
Conferir chave e ambiente do ERP |
| C-Plus 5 indisponível | Falha de rede, timeout ou outro erro | Tentar novamente |
O código inclui os erros de externo para servir de base a quem adaptar este exemplo. Este plugin usa apenas publica e cplus5.
Plugins hospedados rodam num iframe com origem isolada. Não use localStorage, sessionStorage, IndexedDB nem document.cookie: o navegador bloqueia essas APIs nesse contexto. Este exemplo guarda estado apenas em memória.
Scripts de módulo e fontes de um servidor próprio precisam de Access-Control-Allow-Origin: *, porque a origem do iframe é opaca. O <script> comum deste projeto não precisa desse cabeçalho. Diálogos nativos (alert, confirm e prompt), downloads iniciados pelo plugin e APIs como câmera e geolocalização também são bloqueados pelo sandbox. Use openAlert para avisos. O host atual responde true a openConfirmDialog sem mostrar diálogo, então faça confirmações na interface do plugin.
Qualquer pessoa com o endereço de uma versão hospedada pode baixar os arquivos dela. A validação avisa sobre padrões comuns de credencial, mas não bloqueia o envio. Revise o pacote antes de publicá-lo.
node --check plugin.js confere a sintaxe. Rode os testes com node --test plugin.test.mjs. Eles conferem as permissões, a tradução dos erros e as respostas antigas após trocar de atendimento, sem chamar APIs. Para conferir o pacote, envie o .zip pela tela Versões. O servidor aplica as regras de manifesto e arquivo antes de criar a versão.
A skill criar-plugin-novus orienta um agente de IA a criar ou adaptar um plugin. Ela cobre o protocolo, as superfícies, o manifesto, as chamadas de API pelo proxy e a verificação. Peça, por exemplo: "Crie um plugin que adicione uma ação em ticket-menu e mostre o código do ticket".
Ao abrir este repositório no Codex ou no Gemini CLI, a skill em .agents/skills já fica disponível. O Claude Code usa o link em .claude/skills. Para usar a skill em outros projetos, copie a pasta conforme a ferramenta:
| Ferramenta | Pasta para instalar na sua conta |
|---|---|
| Codex | ~/.agents/skills/criar-plugin-novus/ |
| Claude Code | ~/.claude/skills/criar-plugin-novus/ |
| Gemini CLI | ~/.gemini/skills/criar-plugin-novus/ |
Copie a pasta inteira, incluindo SKILL.md. Depois, peça para criar um plugin do Novus CRM ou cite a skill pelo nome. No Claude Code, use /criar-plugin-novus. No Codex, use $criar-plugin-novus. No Gemini CLI, confira a instalação com /skills list.
| Ferramenta | Como usar |
|---|---|
| ChatGPT | Abra Plugins > Skills > Create > Upload from your computer e envie o ZIP da skill gerado abaixo. |
| Claude | Gere o arquivo ZIP com o comando abaixo. Em Customize > Skills, escolha Create skill > Upload a skill e envie o ZIP. |
| Gemini Spark | Abra Skills > Upload e selecione SKILL.md. Revise o conteúdo e clique em Create. |
Para gerar o ZIP da skill, rode na raiz deste repositório:
(cd .agents/skills && zip -r ../../criar-plugin-novus.zip criar-plugin-novus)O ZIP contém a pasta criar-plugin-novus/ com SKILL.md dentro dela. Ele serve para instalar a skill no agente de IA. Não envie esse ZIP como pacote do plugin ao Novus CRM.
O OpenRouter encaminha mensagens para modelos e não instala esta pasta por conta própria. Na aplicação que chama a API, carregue o texto de SKILL.md na mensagem system ou developer e coloque seu pedido na mensagem user. Em qualquer chat sem instalação de skills, anexe SKILL.md ou cole seu conteúdo antes de pedir o plugin. Se o modelo não puder editar arquivos, peça todos os arquivos do plugin e salve-os no projeto.
O guia na documentação do Novus CRM mostra os mesmos passos e links das plataformas.
Consulte a documentação do protocolo. Veja também as páginas de publicação e CLI/API.
