Appearance
Lancer une autre appli ou un widget et attendre sa réponse (kapp.apps)
Une appli (ou un widget) peut en lancer une autre cible pour une action ponctuelle, puis récupérer la data que celle-ci renvoie quand elle se termine. Deux types de cible :
- une appli
oneshotinstallée, désignée par son slug →kapp.apps.runApp(slug); - un widget du channel, désigné par son id numérique →
kapp.apps.runWidget(id).
Le tout via le service kapp.apps.
À ne pas confondre avec
kapp.services.call()(services.md) :callappelle une fonction d'une appli déjà lancée ; les lanceurs dekapp.appslancent une cible dédiée, la laissent vivre (elle peut afficher une UI, faire interagir les viewers…) et résolvent avec la data renvoyée à sonfinish(result).
Le lanceur générique — kapp.apps.run(target)
run prend une cible discriminée et dispatche tout seul vers runApp/runWidget :
ts
const r = await kapp.apps.run(target, args?, options?);
// target = { type: 'app', slug: 'color-picker' }
// | { type: 'widget', id: 23 }
// r: { resolved: boolean, data?: any, reason?: string }C'est exactement la forme produite par l'input de préférence runTarget : le streamer choisit la cible (appli ou widget) dans les réglages de ton appli, et tu passes l'objet tel quel à run — sans brancher toi-même sur le type.
ts
// la cible vient des préférences de ton appli, pas codée en dur
const cible = kapp.preferences.get('cible'); // { type: 'app', slug } | { type: 'widget', id } | null
if (cible) {
const r = await kapp.apps.run(cible, { mise: 100 });
if (r.resolved) console.log(r.data);
}targetmal formé (type inconnu,slug/idmanquant) →{ resolved: false, reason: 'invalid_target' }.- Un
idde widget arrivant en string (valeurs de form/préférence) est converti automatiquement. - Pour plusieurs cibles (préférence de type collection), boucle :
for (const t of cibles) await kapp.apps.run(t, args).
L'input de préférence runTarget
Déclare un item de type runTarget dans preferences.config.js : le dashboard streamer affiche un picker mixant les applis oneshot installées sur le channel et les widgets du channel, et la valeur lue côté appli est déjà une cible prête pour run.
js
// preferences.config.js
cible: {
type: 'runTarget',
label: { en: 'Target to run', fr: 'Cible à lancer' },
},
cibleApp: { // restreint aux applis oneshot
type: 'runTarget',
label: { en: 'App to run', fr: 'Appli à lancer' },
wuOptions: { targets: 'app' },
},
ciblesBonus: { // plusieurs cibles → collection
type: 'collection',
label: { en: 'Bonus targets', fr: 'Cibles bonus' },
collectionItems: { type: 'runTarget' },
},- Valeur hydratée :
{ type: 'app', slug: '...' }|{ type: 'widget', id: 12 }|nullsi le streamer n'a rien choisi — gère toujours ce cas. wuOptions.targets: 'app' | 'widget'restreint le picker à un seul type de cible (sinon les deux, séparés en deux groupes).defaultest interdit (l'id d'un widget est local au channel, non portable) et l'item n'est jamaisrequired: prévois le comportement « pas de cible ».- En
collection, la valeur est un tableau de cibles. - Un widget désactivé reste sélectionnable dans le picker (marqué comme tel) ; à l'exécution
runrenverra{ resolved: false, reason: 'disabled' }tant qu'il est off. kapp.preferences.set()accepte le même objet cible en data pour un cheminrunTarget.
Les raccourcis typés — runApp / runWidget
Quand tu sais déjà quel type tu veux (cible codée en dur, découverte via list), utilise directement :
ts
const r = await kapp.apps.runApp(appliSlug, args?, options?); // appli oneshot, par slug
const r = await kapp.apps.runWidget(widgetId, args?, options?); // widget, par id| Paramètre | Rôle |
|---|---|
appliSlug (string) | Slug de l'appli à lancer (identifiant public stable, ex. color-picker — pas un nombre). Elle doit être installée sur le channel et de type oneshot. Découvre-le via kapp.apps.listApps(). |
widgetId (number) | Id numérique du widget, local au channel (pas portable d'un channel à l'autre). Découvre-le via kapp.apps.listWidgets() (ou laisse le streamer le choisir via un input de préférence runTarget). |
args (objet) | Arguments transmis à la cible. Ils passent par son schéma d'arguments déclaré (typage + validation required), puis sont dispo là-bas via kapp.arguments. Les clés non déclarées par la cible sont ignorées. |
options.timeoutSeconds | Délai max d'attente avant de résoudre {resolved:false, reason:'timeout'}. Défaut 120. |
Le résultat — { resolved, data?, reason? }
Aucun lanceur ne lève d'exception : ils résolvent toujours un objet.
- Succès :
{ resolved: true, data }—data= ce que la cible a passé àkapp.finish(result). - Échec :
{ resolved: false, reason }avecreason∈ :- communes :
aborted(la cible a faitkapp.abort()),no_result(finish()sans data),timeout(pas terminée dans le délai),argument '…' is required/… must be of type …(validation desargs),invalid_target(objettargetmalformé,runseulement). - apps (
runApp) :busy(une instance tourne déjà — unicité garantie),not_installed/not_found(appli absente),not_oneshot(pas enlaunch-mode: oneshot),invalid_output(leresultne respecte pas le type de sortie déclaré, voirrun-output). - widgets (
runWidget) :not_found(id inconnu ou pas sur ce channel),disabled(widget désactivé). Pas debusy(concurrence autorisée), pas deinvalid_output(sortie non typée).
- communes :
ts
const r = await kapp.apps.runApp('color-picker', { question: 'Choisis une couleur' }, { timeoutSeconds: 60 });
if (r.resolved) {
console.log('réponse du viewer :', r.data); // ex. { color: 'red' }
} else {
console.log('pas de réponse :', r.reason); // 'busy' | 'timeout' | 'aborted' | …
}Côté cible — renvoyer la data avec finish
Une appli oneshot
Une appli ordinaire. Pour recevoir des args, elle doit les déclarer dans ses arguments d'app (config/trigger_arguments.config.json) — même schéma qu'un trigger configuré par le streamer. Les arguments déclarés arrivent typés (Player→User, int, bool, string, string_list→string[]) dans kapp.arguments. Elle fait son travail, puis renvoie sa data via finish :
ts
(async function () {
const question = kapp.arguments.question; // argument DÉCLARÉ (clé "question")
kapp.display();
const color = await askViewer(question); // ta logique (UI, vote, interaction…)
await kapp.finish({ color }); // renvoie la data à l'appelant
})();Un widget
Le widget doit avoir un script (script_enabled) qui appelle kapp.finish(result) :
js
window.executeWidgetFunction = async function () {
const choix = await demandeAuViewer(kapp.arguments.question);
kapp.finish({ choix }); // ← remonte à l'appelant comme { resolved: true, data }
};Le widget s'affiche dans son propre overlay configuré (indépendant de l'appelant), comme tout déclenchement de ce widget.
Règles communes
await kapp.finish(result):result(n'importe quelle valeur JSON) remonte comme{ resolved: true, data: result }.kapp.finish()sans argument →{ resolved: false, reason: 'no_result' }.kapp.abort('raison')→{ resolved: false, reason: 'raison' }.resultn'a d'effet que si la cible a été lancée viakapp.apps(sinon il est ignoré).
Listes d'arguments — type string_list
string_list est un type d'argument de trigger comme les autres (catalogue dans ../../widgets/reference/anatomy.md) — utilisable pour n'importe quel trigger (commande chat, etc.), apps comme widgets. Pour recevoir plusieurs valeurs, déclare un argument string_list (= string[]). Sa coercion accepte deux sources :
- lancé par une app/widget : passe un vrai tableau →
kapp.apps.runApp('random-pick', { options: ['a','b','c'] })(solide, pas de parsing). - chat / reward : la string est splittée par un séparateur (défaut
,, configurable viaseparatorcôté appli ; les widgets utilisent toujours,). Ex.!pick a, b, c→['a','b','c'].
Déclaration appli (config/trigger_arguments.config.js) :
js
module.exports = [
{ name: 'Options', key: 'options', type: 'string_list', separator: ',', required: true }
];Déclarer le format de sortie — run-output (apps)
Dans infos.json, une appli peut déclarer le type de son result ; le backend le valide au finish (mismatch → {resolved:false, reason:'invalid_output'}). Types : string, string_list, int, bool, object (JSON libre, pas de contrainte), void.
json
{ "run-output": { "type": "string", "description": "La valeur choisie" } }Les widgets n'ont pas de
run-output: leur sortie n'est pas typée/validée.
Contrats standards — kind (apps)
Plutôt que déclarer args + output séparément, une appli peut adopter un kind (contrat clé-en-main, in + out prédéfinis) via "kind" dans infos.json. Le kind prime sur les déclarations fichier.
kind | Entrée | Sortie |
|---|---|---|
pick-string | options (string_list, requis) | string |
Appli pick-string (cible) :
jsonc
// infos.json
{ "launch-mode": "oneshot", "kind": "pick-string" }js
// script.js
(async function () {
const opts = kapp.arguments.options; // string[] (fourni par le kind)
await kapp.finish(opts[Math.floor(Math.random() * opts.length)]);
})();Appelant :
ts
const r = await kapp.apps.runApp('random-pick', { options: ['rouge', 'vert', 'bleu'] });
// r = { resolved: true, data: 'vert' }Découvrir les cibles lançables
Trois entrées de découverte :
ts
const all = await kapp.apps.list(); // apps + widgets, chaque entrée taguée { type }
const apps = await kapp.apps.listApps(); // applis oneshot seulement
const widgets = await kapp.apps.listWidgets(); // widgets seulementlistApps()→[{ slug, title, version, kind, arguments: [{ name, key, type, required, separator }], output: { type, description } }, …](schéma d'entrée et de sortie déclaré).listWidgets()→[{ id, name, enabled, arguments: [{ name, key, type, description, required }] }, …].list()→ la concaténation des deux, chaque entrée préfixée detype: 'app' | 'widget'(même forme que la cible derun). Filtre partypeau besoin.
ts
// trouver une appli par contrat, puis la lancer
const apps = await kapp.apps.listApps();
const picker = apps.find(a => a.kind === 'pick-string');
if (picker) {
const r = await kapp.apps.runApp(picker.slug, { options: ['a', 'b', 'c'] });
}
// vue unifiée
const all = await kapp.apps.list();
const cibles = all.filter(x => x.type === 'widget');Pièges
- L'appelant a besoin du scope
apps:run(infos.json→scopes) — pour tous les lanceurs et listes, apps comme widgets. - La cible doit déclarer ses arguments pour les recevoir : sans déclaration,
run/runApp/runWidget(args)ne lui transmet rien. - Unicité des apps : si l'appli cible tourne déjà,
runApprépondbusy— gère ce cas. (Les widgets, eux, autorisent la concurrence.) - Id de widget non portable : ne le code pas en dur, découvre-le via
listWidgets()ou laisse le streamer le choisir via un input de préférencerunTarget. - Garde la cible bornée : elle doit appeler
finish()/abort(), sinon l'appelant attend jusqu'autimeout.