Salta ai contenuti

Creare un plugin

I plugin estendono XIQUIL senza modificare il core. Un plugin puo aggiungere API FastAPI, tabelle proprie, una pagina React nel menu laterale, pacchetti Python, pacchetti di sistema e hook di lifecycle.

Questa pagina descrive il flusso reale usato dal plugin manager del core: pacchetto .tar.gz, manifest runtime plugin.json installato, eventuale router.py, eventuale plugin.py, eventuale frontend/bundle.js. Per i pacchetti inviati all’Hub, il manifest sorgente consigliato e plugin.source.json.

Scegli un plugin quando la funzionalita:

  • ha un ciclo di rilascio indipendente dal core;
  • richiede dipendenze Python o pacchetti OS non necessari a tutti gli utenti;
  • aggiunge una pagina o API specialistiche;
  • deve essere distribuita tramite Marketplace;
  • puo vivere in tabelle proprie, senza migrazioni Alembic del core.

Scegli invece una modifica al core quando stai cambiando un concetto centrale di XIQUIL, una permission comune, un modello condiviso o un flusso usato da molte aree dell’app.

In produzione i plugin installati finiscono in:

/app/data/plugins/
├── nome_plugin/
│ ├── plugin.json
│ ├── router.py
│ ├── plugin.py
│ ├── frontend/
│ │ └── bundle.js
│ ├── requirements.txt
│ ├── templates/
│ └── eventuali_pacchetti_python/
└── site/

Durante l’installazione XIQUIL:

  1. estrae il .tar.gz in staging;
  2. valida plugin.json;
  3. installa un eventuale .whl in /app/data/plugins/site;
  4. installa un eventuale requirements.txt nello stesso site directory;
  5. copia plugin.json, router.py, plugin.py, pacchetti Python, templates/ e frontend/bundle.js in /app/data/plugins/{name};
  6. registra eventuali system_packages;
  7. salva lo stato in database come installed_pending_restart.

Le modifiche diventano operative dopo un riavvio del server. Se ci sono pacchetti di sistema mancanti, il restart e completo del container; altrimenti viene riavviato solo uvicorn.

xq_demo_widget/
├── plugin.source.json
├── router.py
├── plugin.py
├── requirements.txt
└── frontend/
└── bundle.js

router.py serve per esporre API sotto /api/plugins/{name}. plugin.py serve per hook di lifecycle, creazione tabelle e registrazione nav. frontend/bundle.js serve per una pagina React caricata dinamicamente da XIQUIL.

Per upload Hub, plugin.source.json contiene solo i campi tecnici. Versione, descrizione pubblica, tier e compatibilità Core si impostano nel form Hub e verranno inseriti nel plugin.json firmato.

Esempio:

{
"name": "xq_demo_widget",
"author": "Il tuo nome",
"depends_on": [],
"provides_routes": true,
"provides_models": false,
"provides_frontend": true,
"icon": "InfoIcon",
"nav_category": "sistema",
"nav_path": "/plugins/xq_demo_widget",
"permissions": [],
"settings_schema": [],
"system_packages": []
}
CampoObbligatorioNote
nameSiIdentificatore snake_case. Deve combaciare con lo slug registrato sull’Hub
authorSiAutore o organizzazione
depends_onNoDipendenze da altri plugin
provides_routesNotrue se esiste un router.py o un hook register_routes
provides_modelsNotrue se il plugin crea tabelle proprie
provides_frontendNotrue se esiste frontend/bundle.js
iconNoNome icona MUI esposta dal core, ad esempio InfoIcon
nav_categoryNoGruppo menu laterale, ad esempio fondi, movimenti, fiscalita, sistema
nav_pathNoDefault: /plugins/{name}
permissionsNoPermessi core che il plugin usa o dichiara
settings_schemaNoSchema impostazioni future del plugin
system_packagesNoPacchetti apt installati al restart completo

Durante sviluppo locale puoi usare ancora un plugin.json completo per installazioni manuali. Nel Marketplace ufficiale, invece, l’Hub genera il plugin.json finale. Vedi Manifest e metadata plugin.

Non duplicare nel manifest sorgente i campi gestiti dall’Hub: version, display_name, description, required_tier, min_core_version e max_core_version. Se devi cambiare uno di questi valori, aggiornalo nel form Hub della bozza/versione.

Le route del plugin vengono montate automaticamente sotto /api/plugins/{name}. Se il plugin si chiama xq_demo_widget, l’endpoint /ping qui sotto risponde a /api/plugins/xq_demo_widget/ping.

from datetime import datetime, timezone
from fastapi import APIRouter, Depends
from app.auth.dependencies import LIVELLO_SOLA_LETTURA, require_permission
router = APIRouter()
@router.get("/ping")
def ping():
return {
"pong": True,
"plugin": "xq_demo_widget",
"version": "1.0.0",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
@router.get(
"/summary",
dependencies=[Depends(require_permission("fondi", LIVELLO_SOLA_LETTURA))],
)
def summary():
return {
"title": "Demo Widget",
"items": [
{"label": "API plugin", "value": "attiva"},
{"label": "Permessi core", "value": "fondi: sola lettura"},
],
}

Usa le dependency del core per permessi, sessione e utente quando accedi a dati XIQUIL. Un plugin non deve esporre dati cross-ambito senza controllare lo scope dell’utente.

plugin.py puo implementare gli hook del Plugin SDK. Per plugin filesystem e upload da Marketplace, XIQUIL fornisce lo shim xq_plugin_sdk con hookimpl.

from xq_plugin_sdk import hookimpl
class XqDemoWidgetPlugin:
@hookimpl
def on_startup(self, app, db_engine):
# Qui puoi creare tabelle plugin_* in modo idempotente.
# Evita migrazioni Alembic del core: i plugin gestiscono il proprio schema.
pass
@hookimpl
def register_routes(self):
from . import router
return [router.router]
@hookimpl
def on_shutdown(self):
pass

Per i plugin filesystem il core monta gia router.py; l’hook register_routes resta utile per compatibilita con il modello Pluggy ed entry point. Le voci menu non si registrano da plugin.py: dichiarale nel manifest con nav_category e nav_path. Tieni gli hook idempotenti: con piu worker il lifecycle puo essere invocato piu volte, anche se il core serializza on_startup dei plugin filesystem con advisory lock PostgreSQL.

Il frontend del plugin e un modulo ES caricato da:

/plugins/{name}/frontend/bundle.js?v={version}

Il bundle non deve includere React, ReactDOM o MUI. Usa i globals esposti dal core in window.__XQ__.

const { React, mui, muiIcons } = window.__XQ__;
const { Box, Button, Stack, Typography, Alert } = mui;
const { Refresh } = muiIcons;
let apiClient = null;
function DemoWidgetPage() {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState("");
const load = React.useCallback(async () => {
if (!apiClient) {
setError("Plugin non ancora inizializzato.");
return;
}
try {
setError("");
const response = await apiClient.get("/api/plugins/xq_demo_widget/summary");
setData(response.data);
} catch (err) {
setError("Impossibile caricare i dati del plugin.");
}
}, []);
React.useEffect(() => {
load();
}, [load]);
return React.createElement(
Stack,
{ spacing: 2 },
React.createElement(
Box,
{ display: "flex", alignItems: "center", justifyContent: "space-between" },
React.createElement(Typography, { variant: "h5" }, "Demo Widget"),
React.createElement(
Button,
{ startIcon: React.createElement(Refresh), onClick: load },
"Aggiorna",
),
),
error && React.createElement(Alert, { severity: "error" }, error),
data && React.createElement(
Stack,
{ spacing: 1 },
data.items.map((item) =>
React.createElement(
Typography,
{ key: item.label, variant: "body2" },
`${item.label}: ${item.value}`,
),
),
),
);
}
export default DemoWidgetPage;
export function setup(context) {
apiClient = context.apiClient;
}

La pagina plugin viene renderizzata da PluginHostPage quando l’utente apre nav_path. Il componente default non riceve props: se ti servono apiClient o navigate, catturali in setup(context) come nell’esempio.

Per dipendenze Python pure:

requirements.txt
httpx>=0.27
python-dateutil>=2.9

Per pacchetti OS, dichiara system_packages nel manifest:

"system_packages": [
{
"name": "libpango-1.0-0",
"optional": false,
"description": "Text rendering engine"
}
]

XIQUIL registra i pacchetti e li installa al prossimo restart completo del container. I pacchetti rimossi da un plugin non vengono disinstallati immediatamente: appaiono come orfani nella sezione amministrativa.

Per testare velocemente in sviluppo:

  1. avvia XIQUIL in modalita development;
  2. abilita il tab sviluppatore con una licenza plugin_developer=true oppure con PLUGIN_DEVELOPER_MODE=True;
  3. crea la cartella plugin;
  4. pacchettizza in .tar.gz;
  5. carica il pacchetto da Marketplace plugin > Sviluppatore > Installa da file;
  6. riavvia quando richiesto;
  7. controlla Installati, log e endpoint /api/plugins/{name}/ping.

Se stai lavorando nella repo xiquil-plugins, puoi usare lo script:

Terminal window
./package.sh xq_demo_widget 1.0.0

Lo script usa la versione passata come argomento ed esclude cache e test. Il tarball mantiene sia plugin.source.json sia plugin.json: il primo descrive le capability tecniche sottoposte a review, mentre il secondo rende il pacchetto verificabile e installabile nei flussi locali previsti. Durante l’approvazione Marketplace l’Hub non si fida dei metadata commerciali presenti nel file caricato: ricostruisce e sostituisce plugin.json con il manifest effettivo derivato dai dati revisionati, quindi firma l’archivio normalizzato.

xq_demo_widget-1.0.0.tar.gz
  • plugin.source.json valido;
  • name uguale allo slug registrato sull’Hub;
  • versione, tier e compatibilità Core compilati nel form Hub;
  • endpoint protetti con permission core adeguate;
  • tabelle plugin con prefisso plugin_{name}_;
  • on_startup idempotente;
  • frontend/bundle.js non include React o MUI;
  • dipendenze Python in requirements.txt o wheel incluso;
  • pacchetti OS dichiarati in system_packages;
  • log utili, senza dati sensibili;
  • pacchetto .tar.gz testato in una istanza pulita.

Nel repository dei plugin ufficiali trovi due modelli utili:

  • xq_bank_reconciliation: API, tabelle proprie, lifecycle hook, frontend React e permessi per ambito;
  • xq_pdf_report: API backend, template, dipendenze Python e pacchetti OS per generazione PDF.

Studiali come riferimento quando devi capire dove mettere router, servizi, modelli, template e bundle frontend.