Spécification des flux d'alerte
Un AlertFlow est la structure de données derrière chaque alerte construite dans le Créateur d'alertes. C'est un arbre d'étapes, pas un graphe nœuds/arêtes : chaque flux a un seul déclencheur et une liste ordonnée d'étapes évaluées de haut en bas.
Les types canoniques vivent dans app/src/shared/types/alert-flow.ts ; l'évaluation dans app/src/main/alerts/flowEvaluator.ts.
AlertFlow
interface AlertFlow {
id: string;
name: string;
enabled: boolean;
trigger: AlertTrigger;
root: Step[];
createdAt: string;
updatedAt: string;
}Seuls les flux avec enabled: true sont chargés et liés au runtime. Le tableau root est le point d'entrée de l'évaluation.
AlertTrigger
interface AlertTrigger {
type: AlertTriggerType;
params?: Record<string, unknown>;
}type | Chemin source | params |
|---|---|---|
follow | EventSub | - |
subscribe | EventSub | - |
cheer | EventSub | - |
raid | EventSub | - |
gift_sub | EventSub | - |
welcome | EventSub | - |
channel_point | FlowEngine (redemption) | { rewardId } (absent = toute récompense) |
cron | FlowEngine (scheduler) | { schedule, onlyWhileLive } |
Pour cron, onlyWhileLive vaut true par défaut (le job ne se déclenche que pendant le live). Pour channel_point, omettre rewardId fait se déclencher le flux à chaque échange d'une récompense personnalisée.
Step
Un Step prend l'une des trois formes discriminées :
type Step =
| { kind: "display"; id: string; display: AlertDisplay }
| {
kind: "switch";
id: string;
variable: string;
cases: { match: string; steps: Step[] }[];
default: Step[];
}
| {
kind: "if";
id: string;
variable: string;
op: ConditionOp;
value: string;
then: Step[];
else: Step[];
};display: pousse sonAlertDisplaydans la liste de sortie.switch: résoutvariabledans le contexte d'event, la convertit en chaîne, puis exécute le premiercasedontmatchest égal à cette chaîne (comparaison de chaînes exacte). Si aucun ne correspond, exécutedefault.if: comparevariableàvalueviaop, puis exécutethenouelse.
Les étapes s'imbriquent librement : un case de switch ou une branche de if contient son propre Step[].
ConditionOp
type ConditionOp = "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains";Sémantique de comparaison (compare() dans flowEvaluator.ts) : les opérandes sont d'abord convertis en chaînes. eq / neq / contains opèrent toujours sur des chaînes. Pour gt / lt / gte / lte, si les deux opérandes se parsent en nombres finis, la comparaison est numérique ; sinon elle retombe sur l'ordre lexicographique (donc "9" > "10" vaut true). Une variable absente résout vers la chaîne vide.
AlertDisplay
interface AlertDisplay {
phrase: string;
htmlTemplate?: string;
assetId?: string;
imageUrl?: string; // résolu au runtime, jamais persisté
durationMs: number;
volume: number; // 0..1
animaleseEnabled?: boolean;
animalesePitch?: number;
}assetId référence un asset de la Médiathèque. Au déclenchement, resolveDisplayImage() retrouve l'asset et fixe imageUrl au nom de fichier que l'overlay peut charger. Voir le Protocole d'assets. imageUrl n'est jamais stocké dans SQLite.
Évaluation
evaluateFlow(flow, ctx) parcourt flow.root et retourne un AlertDisplay[] ordonné. La fonction est pure et ne throw jamais : une variable non résolue, un switch sans match et au default vide, ou une branche malformée produisent simplement moins d'affichages. Les étapes s'exécutent dans l'ordre du tableau ; le runtime n'émet actuellement que le premier affichage produit.
Le contexte d'évaluation est indexé sous event, par exemple event.tier, event.userName, event.bits. TRIGGER_VARIABLE_HINTS liste les variables mises en avant par déclencheur dans le picker de l'éditeur (toutes restent accessibles).
Chemins d'exécution
Deux chemins alimentent evaluateFlow, tous deux dans app/src/main/handlers/twitch.ts :
- Chemin EventSub (
processAlert) : follow / subscribe / cheer / raid / gift_sub / welcome. Un event Twitch entrant est confronté aux flux activés du mêmetrigger.type; un flux correspondant décore l'alerte avec son affichage. Les types réservés aux flux (channel_point,cron) sont rejetés ici. - Chemin flow trigger (
handleFlowTrigger, actionalerts:flow_trigger) : les fluxchannel_pointetcronsont liés par leFlowEngine(app/src/main/flows/engine.ts), qui dispatchetwitch / alerts:flow_triggerau déclenchement. Le bouton de test de l'éditeur utilise le même chemin avec un event synthétiqueTestUser. Ici le flux est la source : s'il ne produit aucun affichage, rien n'est émis.
L'alerte résultante porte TwitchAlertData.flowId (l'id du flux qui a produit l'affichage). Un overlay restreint à un flux peut filtrer dessus.
Filtre de flux côté overlay
L'overlay intégré twitch-alerts lit un paramètre de requête ?flow=<id>. Quand il est présent, l'overlay n'enfile que les alertes dont lastAlert.flowId est égal à cet id ; sans lui, l'overlay affiche toutes les alertes (file partagée). Cela permet à un seul flux de piloter une source navigateur OBS dédiée.
Exemple de flux
Un flux d'abonnement qui switche sur le tier :
{
"id": "flow_sub_tiers",
"name": "Sub tiers",
"enabled": true,
"trigger": { "type": "subscribe" },
"root": [
{
"kind": "switch",
"id": "s1",
"variable": "event.tier",
"cases": [
{
"match": "3000",
"steps": [
{
"kind": "display",
"id": "d3",
"display": {
"phrase": "{{userName}} passe en Tier 3 !",
"assetId": "tier3gifAB",
"durationMs": 6000,
"volume": 0.9
}
}
]
},
{
"match": "2000",
"steps": [
{
"kind": "display",
"id": "d2",
"display": {
"phrase": "{{userName}} s'abonne en Tier 2 !",
"durationMs": 5000,
"volume": 0.8
}
}
]
}
],
"default": [
{
"kind": "display",
"id": "d1",
"display": {
"phrase": "Merci pour l'abo, {{userName}} !",
"durationMs": 4000,
"volume": 0.7
}
}
]
}
],
"createdAt": "2026-06-07T10:00:00.000Z",
"updatedAt": "2026-06-07T10:00:00.000Z"
}EventSub fixe event.tier à "1000", "2000" ou "3000". Le Tier 1 retombe sur default.
