ConectIQ · documentação
Ver markdown

Plugin SDK

Toda funcionalidade nova entra como plugin. O núcleo dispara hooks; o plugin se conecta a eles. Nenhum plugin precisa (nem deve) alterar arquivos do núcleo.


Anatomia

plugins/meu-plugin/
  plugin.json     manifesto
  Plugin.php      classe que implementa PluginInterface
  src/            classes extras (autoload: Plugins\MeuPlugin\Sub\Classe)
  views/          templates próprios

plugin.json

{
  "name": "Meu Plugin",
  "version": "1.2.0",
  "description": "O que ele faz, em uma frase.",
  "author": "Seu Nome",
  "entry": "Plugin.php",
  "class": "Plugins\\MeuPlugin\\Plugin",
  "priority": 50,
  "permissions": ["hooks", "routes", "gateways", "alerts", "ai", "queue", "database"],
  "requires": {
    "php": "8.1",
    "core": "1.0.0",
    "extensions": ["curl"],
    "plugins": { "outro-plugin": ">=2.0" }
  },
  "default_config": { "api_key": "" }
}

Permissões são obrigatórias: usar um recurso sem declará-lo lança exceção.

PermissãoLibera
hookson(), filter()
routesroute(), apiRoute()
gatewaysgateway()
alertsalertChannel()
aiaiTool(), aiContext()
queuejob()
databaseacesso direto ao banco

Plugin.php

<?php
namespace Plugins\MeuPlugin;

use App\Core\Plugin\PluginInterface;
use App\Core\Plugin\Sdk;

class Plugin implements PluginInterface
{
    public function boot(Sdk $sdk): void { /* roda a cada requisição */ }
    public function install(Sdk $sdk): void { /* uma vez, na instalação */ }
    public function upgrade(Sdk $sdk, string $fromVersion): void { /* ao subir versão */ }
    public function uninstall(Sdk $sdk): void { /* limpeza */ }
}

O upgrade() roda sozinho quando a versão do plugin.json for maior que a instalada. boot() precisa ser barato — só registra; não faça I/O pesado.


Referência do SDK

Eventos e filtros

$sdk->on('order.approved', fn(array $order) => /* … */);
$sdk->filter('metrics.derive', fn(array $m) => $m + ['minha_metrica' => 1]);
$sdk->emit('meu.evento', $dados);

Eventos disponíveis

EventoQuandoRecebe
app.bootedFim do bootKernel
tracking.event (filtro)Antes de enfileirar a visitaarray $event
tracking.ingestedVisita gravadaarray $event
tracking.geo (filtro)Resolução geográficaarray $geo, string $ip
order.before_save (filtro)Antes de gravar o pedidoarray $data
order.attribution (filtro)Após resolver a atribuiçãoarray $attr
order.createdPedido novoarray $order
order.status_changedMudou de statusarray $order, ?string $anterior
order.approved / .refused / .refunded / .chargebackPor statusarray $order
order.reattributedAtribuição corrigidaarray $order, array $attr
alert.before_dispatch (filtro)Antes de enviar (devolva null para cancelar)array $data
alert.dispatchedAlerta criadoint $id, int $projectId, string $type
anomaly.detectedAnomalia encontradaint $id, int $projectId, string $kind
anomaly.signals (filtro)Sinais coletadosarray $signals
report.generatedRelatório prontoint $id, int $projectId
ai.answeredAssistente respondeuint $projectId, string $q, array $result
plugin.installed / .enabled / .disabled / .uninstalledCiclo de vidastring $slug

Filtros de interface

FiltroModifica
admin.menuItens do menu lateral
dashboard.widgetsBlocos do dashboard
ranking.rows / ranking.highlightsLinhas e destaques do ranking
metrics.deriveMétricas calculadas
ai.tool_definitions / ai.tool_result / ai.system_contextComportamento da IA
gateways.all / alerts.channels / pixels.availableRegistros disponíveis
cron.tasksTarefas agendadas
http.responseResposta HTTP final

Rotas e interface

$sdk->route('GET', '/painel', fn($request) => Response::view('meu-plugin::painel'));
$sdk->apiRoute('POST', '/sync', fn($request) => ['ok' => true]);
$sdk->views('views');
$sdk->menu('Meu Plugin', '/plugin/meu-plugin/painel', '★', 70);
$sdk->widget('resumo', 'Meu resumo', fn($project, $period) => '<p>HTML</p>');

Rotas ficam sob /plugin/{slug}/… e /api/plugin/{slug}/… — sem colisão com o núcleo.

Gateway de pagamento

$sdk->gateway('meugateway', 'Meu Gateway',
    fn(array $payload) => OrderNormalizer::generic([
        'order_id' => $payload['id'],
        'status'   => $payload['situacao'],
        'total'    => $payload['valor'],        // na unidade da moeda
        // 'total' => OrderNormalizer::centsToCents($payload['valor_centavos']),
        'tracking' => ['vid' => $payload['custom']['utmi_vid'] ?? null],
    ]),
    [
        'fields' => [['name' => 'secret', 'label' => 'Segredo', 'type' => 'password']],
        'verify' => fn($payload, $raw, $headers, $config) =>
            hash_equals(hash_hmac('sha256', $raw, $config['secret']), $headers['x-signature'] ?? ''),
    ]
);

A URL do webhook passa a ser /webhook/meugateway/{chave-secreta-do-projeto}.

Canal de alerta

$sdk->alertChannel('slack', 'Slack',
    function (array $alert, array $config): bool {
        $r = Http::postJson($config['webhook_url'], ['text' => $alert['title']]);
        if ($r['status'] !== 200) {
            throw new \RuntimeException('Slack respondeu ' . $r['status']);
        }
        return true;   // lançar exceção faz a fila tentar de novo com backoff
    },
    [['name' => 'webhook_url', 'label' => 'Webhook', 'type' => 'url', 'required' => true]]
);

Ferramenta de IA

$sdk->aiTool(
    'get_estoque',
    'Consulta o estoque atual dos produtos. Use quando a pergunta for sobre '
    . 'disponibilidade, ruptura ou produto esgotado.',
    ['type' => 'object', 'properties' => [
        'produto_id' => ['type' => 'string', 'description' => 'Filtra por produto.'],
    ]],
    fn(array $input, array $context) => Estoque::consultar($context['project_id'], $input)
);

A descrição é o que faz o modelo escolher (ou não) a ferramenta: diga quando usá-la, não só o que ela faz. O $context traz project_id, project, currency e user_id. Devolva arrays — o SDK serializa. Valores monetários: converta para a unidade da moeda antes de devolver.

Fila e agendamento

$sdk->job('sincronizar', fn(array $payload) => /* … */);
$sdk->dispatch('sincronizar', ['id' => 1], Queue::LOW, 60);   // 60s de atraso
$sdk->schedule('diaria', 'daily_at:03:00', fn() => /* … */);

Frequências: every_minute, every_5_minutes, every_10_minutes, every_15_minutes, every_30_minutes, hourly, daily, daily_at:HH:MM, weekly_on:N:HH:MM (1 = segunda), monthly_on:D:HH:MM.

Dados e configuração

$tabela = $sdk->table('registros');       // plugin_meu_plugin_registros
$valor  = $sdk->config('api_key');
$sdk->setConfig(['api_key' => 'novo']);
$sdk->log('mensagem', 'info', ['contexto' => 1]);

Tabelas do plugin devem usar o prefixo de $sdk->table() — é o que permite remover tudo no uninstall() sem tocar em dados do núcleo.


Ciclo de vida e segurança

Publicando

Distribua a pasta do plugin (zip ou git). O usuário coloca em /plugins, abre Plugins no painel e instala em um clique. Versione com SemVer: subir a version no plugin.json dispara o upgrade() na próxima ativação.